Completed
Branch master (7ca37f)
by
unknown
26:17
created

EditPage::setHeaders()   D

Complexity

Conditions 13
Paths 288

Size

Total Lines 54
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 29
c 1
b 0
f 0
nc 288
nop 0
dl 0
loc 54
rs 4.9261

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		# Enabled article-related sidebar, toplinks, etc.
2246
		$wgOut->setArticleRelated( true );
2247
2248
		$contextTitle = $this->getContextTitle();
2249
		if ( $this->isConflict ) {
2250
			$msg = 'editconflict';
2251
		} elseif ( $contextTitle->exists() && $this->section != '' ) {
2252
			$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2253
		} else {
2254
			$msg = $contextTitle->exists()
2255
				|| ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2256
					&& $contextTitle->getDefaultMessageText() !== false
2257
				)
2258
				? 'editing'
2259
				: 'creating';
2260
		}
2261
2262
		# Use the title defined by DISPLAYTITLE magic word when present
2263
		# NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2264
		#       setPageTitle() treats the input as wikitext, which should be safe in either case.
2265
		$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2266
		if ( $displayTitle === false ) {
2267
			$displayTitle = $contextTitle->getPrefixedText();
2268
		}
2269
		$wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2270
		# Transmit the name of the message to JavaScript for live preview
2271
		# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2272
		$wgOut->addJsConfigVars( [
2273
			'wgEditMessage' => $msg,
2274
			'wgAjaxEditStash' => $wgAjaxEditStash,
2275
		] );
2276
	}
2277
2278
	/**
2279
	 * Show all applicable editing introductions
2280
	 */
2281
	protected function showIntro() {
2282
		global $wgOut, $wgUser;
2283
		if ( $this->suppressIntro ) {
2284
			return;
2285
		}
2286
2287
		$namespace = $this->mTitle->getNamespace();
2288
2289
		if ( $namespace == NS_MEDIAWIKI ) {
2290
			# Show a warning if editing an interface message
2291
			$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2292
			# If this is a default message (but not css or js),
2293
			# show a hint that it is translatable on translatewiki.net
2294
			if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2295
				&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2296
			) {
2297
				$defaultMessageText = $this->mTitle->getDefaultMessageText();
2298
				if ( $defaultMessageText !== false ) {
2299
					$wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2300
						'translateinterface' );
2301
				}
2302
			}
2303
		} elseif ( $namespace == NS_FILE ) {
2304
			# Show a hint to shared repo
2305
			$file = wfFindFile( $this->mTitle );
2306
			if ( $file && !$file->isLocal() ) {
2307
				$descUrl = $file->getDescriptionUrl();
2308
				# there must be a description url to show a hint to shared repo
2309
				if ( $descUrl ) {
2310
					if ( !$this->mTitle->exists() ) {
2311
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2312
									'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2313
						] );
2314
					} else {
2315
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2316
									'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2317
						] );
2318
					}
2319
				}
2320
			}
2321
		}
2322
2323
		# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2324
		# Show log extract when the user is currently blocked
2325
		if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2326
			$username = explode( '/', $this->mTitle->getText(), 2 )[0];
2327
			$user = User::newFromName( $username, false /* allow IP users*/ );
2328
			$ip = User::isIP( $username );
2329
			$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 2327 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 2327 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...
2330
			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2331
				$wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2332
					[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2333 View Code Duplication
			} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2334
				# Show log extract if the user is currently blocked
2335
				LogEventsList::showLogExtract(
2336
					$wgOut,
2337
					'block',
2338
					MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2339
					'',
2340
					[
2341
						'lim' => 1,
2342
						'showIfEmpty' => false,
2343
						'msgKey' => [
2344
							'blocked-notice-logextract',
2345
							$user->getName() # Support GENDER in notice
2346
						]
2347
					]
2348
				);
2349
			}
2350
		}
2351
		# Try to add a custom edit intro, or use the standard one if this is not possible.
2352
		if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2353
			$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2354
				wfMessage( 'helppage' )->inContentLanguage()->text()
2355
			) );
2356
			if ( $wgUser->isLoggedIn() ) {
2357
				$wgOut->wrapWikiMsg(
2358
					// Suppress the external link icon, consider the help url an internal one
2359
					"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2360
					[
2361
						'newarticletext',
2362
						$helpLink
2363
					]
2364
				);
2365
			} else {
2366
				$wgOut->wrapWikiMsg(
2367
					// Suppress the external link icon, consider the help url an internal one
2368
					"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2369
					[
2370
						'newarticletextanon',
2371
						$helpLink
2372
					]
2373
				);
2374
			}
2375
		}
2376
		# Give a notice if the user is editing a deleted/moved page...
2377 View Code Duplication
		if ( !$this->mTitle->exists() ) {
2378
			LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2379
				'',
2380
				[
2381
					'lim' => 10,
2382
					'conds' => [ "log_action != 'revision'" ],
2383
					'showIfEmpty' => false,
2384
					'msgKey' => [ 'recreate-moveddeleted-warn' ]
2385
				]
2386
			);
2387
		}
2388
	}
2389
2390
	/**
2391
	 * Attempt to show a custom editing introduction, if supplied
2392
	 *
2393
	 * @return bool
2394
	 */
2395
	protected function showCustomIntro() {
2396
		if ( $this->editintro ) {
2397
			$title = Title::newFromText( $this->editintro );
2398
			if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2399
				global $wgOut;
2400
				// Added using template syntax, to take <noinclude>'s into account.
2401
				$wgOut->addWikiTextTitleTidy(
2402
					'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2403
					$this->mTitle
2404
				);
2405
				return true;
2406
			}
2407
		}
2408
		return false;
2409
	}
2410
2411
	/**
2412
	 * Gets an editable textual representation of $content.
2413
	 * The textual representation can be turned by into a Content object by the
2414
	 * toEditContent() method.
2415
	 *
2416
	 * If $content is null or false or a string, $content is returned unchanged.
2417
	 *
2418
	 * If the given Content object is not of a type that can be edited using
2419
	 * the text base EditPage, an exception will be raised. Set
2420
	 * $this->allowNonTextContent to true to allow editing of non-textual
2421
	 * content.
2422
	 *
2423
	 * @param Content|null|bool|string $content
2424
	 * @return string The editable text form of the content.
2425
	 *
2426
	 * @throws MWException If $content is not an instance of TextContent and
2427
	 *   $this->allowNonTextContent is not true.
2428
	 */
2429
	protected function toEditText( $content ) {
2430
		if ( $content === null || $content === false || is_string( $content ) ) {
2431
			return $content;
2432
		}
2433
2434
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2435
			throw new MWException( 'This content model is not supported: '
2436
				. 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...
2437
		}
2438
2439
		return $content->serialize( $this->contentFormat );
2440
	}
2441
2442
	/**
2443
	 * Turns the given text into a Content object by unserializing it.
2444
	 *
2445
	 * If the resulting Content object is not of a type that can be edited using
2446
	 * the text base EditPage, an exception will be raised. Set
2447
	 * $this->allowNonTextContent to true to allow editing of non-textual
2448
	 * content.
2449
	 *
2450
	 * @param string|null|bool $text Text to unserialize
2451
	 * @return Content The content object created from $text. If $text was false
2452
	 *   or null, false resp. null will be  returned instead.
2453
	 *
2454
	 * @throws MWException If unserializing the text results in a Content
2455
	 *   object that is not an instance of TextContent and
2456
	 *   $this->allowNonTextContent is not true.
2457
	 */
2458
	protected function toEditContent( $text ) {
2459
		if ( $text === false || $text === null ) {
2460
			return $text;
2461
		}
2462
2463
		$content = ContentHandler::makeContent( $text, $this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $text defined by parameter $text on line 2458 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...
2464
			$this->contentModel, $this->contentFormat );
2465
2466
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2467
			throw new MWException( 'This content model is not supported: '
2468
				. ContentHandler::getLocalizedName( $content->getModel() ) );
2469
		}
2470
2471
		return $content;
2472
	}
2473
2474
	/**
2475
	 * Send the edit form and related headers to $wgOut
2476
	 * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2477
	 *     during form output near the top, for captchas and the like.
2478
	 *
2479
	 * The $formCallback parameter is deprecated since MediaWiki 1.25. Please
2480
	 * use the EditPage::showEditForm:fields hook instead.
2481
	 */
2482
	function showEditForm( $formCallback = null ) {
2483
		global $wgOut, $wgUser;
2484
2485
		# need to parse the preview early so that we know which templates are used,
2486
		# otherwise users with "show preview after edit box" will get a blank list
2487
		# we parse this near the beginning so that setHeaders can do the title
2488
		# setting work instead of leaving it in getPreviewText
2489
		$previewOutput = '';
2490
		if ( $this->formtype == 'preview' ) {
2491
			$previewOutput = $this->getPreviewText();
2492
		}
2493
2494
		Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2495
2496
		$this->setHeaders();
2497
2498
		if ( $this->showHeader() === false ) {
2499
			return;
2500
		}
2501
2502
		$wgOut->addHTML( $this->editFormPageTop );
2503
2504
		if ( $wgUser->getOption( 'previewontop' ) ) {
2505
			$this->displayPreviewArea( $previewOutput, true );
2506
		}
2507
2508
		$wgOut->addHTML( $this->editFormTextTop );
2509
2510
		$showToolbar = true;
2511
		if ( $this->wasDeletedSinceLastEdit() ) {
2512
			if ( $this->formtype == 'save' ) {
2513
				// Hide the toolbar and edit area, user can click preview to get it back
2514
				// Add an confirmation checkbox and explanation.
2515
				$showToolbar = false;
2516
			} else {
2517
				$wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2518
					'deletedwhileediting' );
2519
			}
2520
		}
2521
2522
		// @todo add EditForm plugin interface and use it here!
2523
		//       search for textarea1 and textares2, and allow EditForm to override all uses.
2524
		$wgOut->addHTML( Html::openElement(
2525
			'form',
2526
			[
2527
				'id' => self::EDITFORM_ID,
2528
				'name' => self::EDITFORM_ID,
2529
				'method' => 'post',
2530
				'action' => $this->getActionURL( $this->getContextTitle() ),
2531
				'enctype' => 'multipart/form-data'
2532
			]
2533
		) );
2534
2535
		if ( is_callable( $formCallback ) ) {
2536
			wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2537
			call_user_func_array( $formCallback, [ &$wgOut ] );
2538
		}
2539
2540
		// Add an empty field to trip up spambots
2541
		$wgOut->addHTML(
2542
			Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2543
			. Html::rawElement(
2544
				'label',
2545
				[ 'for' => 'wpAntispam' ],
2546
				wfMessage( 'simpleantispam-label' )->parse()
2547
			)
2548
			. Xml::element(
2549
				'input',
2550
				[
2551
					'type' => 'text',
2552
					'name' => 'wpAntispam',
2553
					'id' => 'wpAntispam',
2554
					'value' => ''
2555
				]
2556
			)
2557
			. Xml::closeElement( 'div' )
2558
		);
2559
2560
		Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2561
2562
		// Put these up at the top to ensure they aren't lost on early form submission
2563
		$this->showFormBeforeText();
2564
2565
		if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2566
			$username = $this->lastDelete->user_name;
2567
			$comment = $this->lastDelete->log_comment;
2568
2569
			// It is better to not parse the comment at all than to have templates expanded in the middle
2570
			// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2571
			$key = $comment === ''
2572
				? 'confirmrecreate-noreason'
2573
				: 'confirmrecreate';
2574
			$wgOut->addHTML(
2575
				'<div class="mw-confirm-recreate">' .
2576
					wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2577
				Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2578
					[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2579
				) .
2580
				'</div>'
2581
			);
2582
		}
2583
2584
		# When the summary is hidden, also hide them on preview/show changes
2585
		if ( $this->nosummary ) {
2586
			$wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2587
		}
2588
2589
		# If a blank edit summary was previously provided, and the appropriate
2590
		# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2591
		# user being bounced back more than once in the event that a summary
2592
		# is not required.
2593
		# ####
2594
		# For a bit more sophisticated detection of blank summaries, hash the
2595
		# automatic one and pass that in the hidden field wpAutoSummary.
2596
		if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2597
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2598
		}
2599
2600
		if ( $this->undidRev ) {
2601
			$wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2602
		}
2603
2604
		if ( $this->selfRedirect ) {
2605
			$wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2606
		}
2607
2608
		if ( $this->hasPresetSummary ) {
2609
			// If a summary has been preset using &summary= we don't want to prompt for
2610
			// a different summary. Only prompt for a summary if the summary is blanked.
2611
			// (Bug 17416)
2612
			$this->autoSumm = md5( '' );
2613
		}
2614
2615
		$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2616
		$wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2617
2618
		$wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2619
		$wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2620
2621
		$wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2622
		$wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2623
2624 View Code Duplication
		if ( $this->section == 'new' ) {
2625
			$this->showSummaryInput( true, $this->summary );
2626
			$wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2627
		}
2628
2629
		$wgOut->addHTML( $this->editFormTextBeforeContent );
2630
2631
		if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2632
			$wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2633
		}
2634
2635
		if ( $this->blankArticle ) {
2636
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2637
		}
2638
2639
		if ( $this->isConflict ) {
2640
			// In an edit conflict bypass the overridable content form method
2641
			// and fallback to the raw wpTextbox1 since editconflicts can't be
2642
			// resolved between page source edits and custom ui edits using the
2643
			// custom edit ui.
2644
			$this->textbox2 = $this->textbox1;
2645
2646
			$content = $this->getCurrentContent();
2647
			$this->textbox1 = $this->toEditText( $content );
2648
2649
			$this->showTextbox1();
2650
		} else {
2651
			$this->showContentForm();
2652
		}
2653
2654
		$wgOut->addHTML( $this->editFormTextAfterContent );
2655
2656
		$this->showStandardInputs();
2657
2658
		$this->showFormAfterText();
2659
2660
		$this->showTosSummary();
2661
2662
		$this->showEditTools();
2663
2664
		$wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2665
2666
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2667
			Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2668
2669
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2670
			Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2671
2672
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2673
			self::getPreviewLimitReport( $this->mParserOutput ) ) );
2674
2675
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2676
2677 View Code Duplication
		if ( $this->isConflict ) {
2678
			try {
2679
				$this->showConflict();
2680
			} catch ( MWContentSerializationException $ex ) {
2681
				// this can't really happen, but be nice if it does.
2682
				$msg = wfMessage(
2683
					'content-failed-to-parse',
2684
					$this->contentModel,
2685
					$this->contentFormat,
2686
					$ex->getMessage()
2687
				);
2688
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2689
			}
2690
		}
2691
2692
		// Set a hidden field so JS knows what edit form mode we are in
2693
		if ( $this->isConflict ) {
2694
			$mode = 'conflict';
2695
		} elseif ( $this->preview ) {
2696
			$mode = 'preview';
2697
		} elseif ( $this->diff ) {
2698
			$mode = 'diff';
2699
		} else {
2700
			$mode = 'text';
2701
		}
2702
		$wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2703
2704
		// Marker for detecting truncated form data.  This must be the last
2705
		// parameter sent in order to be of use, so do not move me.
2706
		$wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2707
		$wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2708
2709
		if ( !$wgUser->getOption( 'previewontop' ) ) {
2710
			$this->displayPreviewArea( $previewOutput, false );
2711
		}
2712
2713
	}
2714
2715
	/**
2716
	 * Extract the section title from current section text, if any.
2717
	 *
2718
	 * @param string $text
2719
	 * @return string|bool String or false
2720
	 */
2721
	public static function extractSectionTitle( $text ) {
2722
		preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2723
		if ( !empty( $matches[2] ) ) {
2724
			global $wgParser;
2725
			return $wgParser->stripSectionName( trim( $matches[2] ) );
2726
		} else {
2727
			return false;
2728
		}
2729
	}
2730
2731
	/**
2732
	 * @return bool
2733
	 */
2734
	protected function showHeader() {
2735
		global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
2736
		global $wgAllowUserCss, $wgAllowUserJs;
2737
2738
		if ( $this->mTitle->isTalkPage() ) {
2739
			$wgOut->addWikiMsg( 'talkpagetext' );
2740
		}
2741
2742
		// Add edit notices
2743
		$editNotices = $this->mTitle->getEditNotices( $this->oldid );
2744
		if ( count( $editNotices ) ) {
2745
			$wgOut->addHTML( implode( "\n", $editNotices ) );
2746
		} else {
2747
			$msg = wfMessage( 'editnotice-notext' );
2748
			if ( !$msg->isDisabled() ) {
2749
				$wgOut->addHTML(
2750
					'<div class="mw-editnotice-notext">'
2751
					. $msg->parseAsBlock()
2752
					. '</div>'
2753
				);
2754
			}
2755
		}
2756
2757
		if ( $this->isConflict ) {
2758
			$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2759
			$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...
2760
		} else {
2761
			if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2762
				// We use $this->section to much before this and getVal('wgSection') directly in other places
2763
				// at this point we can't reset $this->section to '' to fallback to non-section editing.
2764
				// Someone is welcome to try refactoring though
2765
				$wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2766
				return false;
2767
			}
2768
2769
			if ( $this->section != '' && $this->section != 'new' ) {
2770
				if ( !$this->summary && !$this->preview && !$this->diff ) {
2771
					$sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2772
					if ( $sectionTitle !== false ) {
2773
						$this->summary = "/* $sectionTitle */ ";
2774
					}
2775
				}
2776
			}
2777
2778
			if ( $this->missingComment ) {
2779
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2780
			}
2781
2782
			if ( $this->missingSummary && $this->section != 'new' ) {
2783
				$wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2784
			}
2785
2786
			if ( $this->missingSummary && $this->section == 'new' ) {
2787
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2788
			}
2789
2790
			if ( $this->blankArticle ) {
2791
				$wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2792
			}
2793
2794
			if ( $this->selfRedirect ) {
2795
				$wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2796
			}
2797
2798
			if ( $this->hookError !== '' ) {
2799
				$wgOut->addWikiText( $this->hookError );
2800
			}
2801
2802
			if ( !$this->checkUnicodeCompliantBrowser() ) {
2803
				$wgOut->addWikiMsg( 'nonunicodebrowser' );
2804
			}
2805
2806
			if ( $this->section != 'new' ) {
2807
				$revision = $this->mArticle->getRevisionFetched();
2808
				if ( $revision ) {
2809
					// Let sysop know that this will make private content public if saved
2810
2811 View Code Duplication
					if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2812
						$wgOut->wrapWikiMsg(
2813
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2814
							'rev-deleted-text-permission'
2815
						);
2816
					} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2817
						$wgOut->wrapWikiMsg(
2818
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2819
							'rev-deleted-text-view'
2820
						);
2821
					}
2822
2823
					if ( !$revision->isCurrent() ) {
2824
						$this->mArticle->setOldSubtitle( $revision->getId() );
2825
						$wgOut->addWikiMsg( 'editingold' );
2826
					}
2827
				} elseif ( $this->mTitle->exists() ) {
2828
					// Something went wrong
2829
2830
					$wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2831
						[ 'missing-revision', $this->oldid ] );
2832
				}
2833
			}
2834
		}
2835
2836
		if ( wfReadOnly() ) {
2837
			$wgOut->wrapWikiMsg(
2838
				"<div id=\"mw-read-only-warning\">\n$1\n</div>",
2839
				[ 'readonlywarning', wfReadOnlyReason() ]
2840
			);
2841
		} elseif ( $wgUser->isAnon() ) {
2842
			if ( $this->formtype != 'preview' ) {
2843
				$wgOut->wrapWikiMsg(
2844
					"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2845
					[ 'anoneditwarning',
2846
						// Log-in link
2847
						SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2848
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2849
						] ),
2850
						// Sign-up link
2851
						SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2852
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2853
						] )
2854
					]
2855
				);
2856
			} else {
2857
				$wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2858
					'anonpreviewwarning'
2859
				);
2860
			}
2861
		} else {
2862
			if ( $this->isCssJsSubpage ) {
2863
				# Check the skin exists
2864
				if ( $this->isWrongCaseCssJsPage ) {
2865
					$wgOut->wrapWikiMsg(
2866
						"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2867
						[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2868
					);
2869
				}
2870
				if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2871
					if ( $this->formtype !== 'preview' ) {
2872
						if ( $this->isCssSubpage && $wgAllowUserCss ) {
2873
							$wgOut->wrapWikiMsg(
2874
								"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2875
								[ 'usercssyoucanpreview' ]
2876
							);
2877
						}
2878
2879
						if ( $this->isJsSubpage && $wgAllowUserJs ) {
2880
							$wgOut->wrapWikiMsg(
2881
								"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2882
								[ 'userjsyoucanpreview' ]
2883
							);
2884
						}
2885
					}
2886
				}
2887
			}
2888
		}
2889
2890
		if ( $this->mTitle->isProtected( 'edit' ) &&
2891
			MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2892
		) {
2893
			# Is the title semi-protected?
2894
			if ( $this->mTitle->isSemiProtected() ) {
2895
				$noticeMsg = 'semiprotectedpagewarning';
2896
			} else {
2897
				# Then it must be protected based on static groups (regular)
2898
				$noticeMsg = 'protectedpagewarning';
2899
			}
2900
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2901
				[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2902
		}
2903
		if ( $this->mTitle->isCascadeProtected() ) {
2904
			# Is this page under cascading protection from some source pages?
2905
			/** @var Title[] $cascadeSources */
2906
			list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2907
			$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2908
			$cascadeSourcesCount = count( $cascadeSources );
2909
			if ( $cascadeSourcesCount > 0 ) {
2910
				# Explain, and list the titles responsible
2911
				foreach ( $cascadeSources as $page ) {
2912
					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2913
				}
2914
			}
2915
			$notice .= '</div>';
2916
			$wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2917
		}
2918
		if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2919
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2920
				[ 'lim' => 1,
2921
					'showIfEmpty' => false,
2922
					'msgKey' => [ 'titleprotectedwarning' ],
2923
					'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2924
		}
2925
2926
		if ( $this->kblength === false ) {
2927
			$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...
2928
		}
2929
2930
		if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2931
			$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2932
				[
2933
					'longpageerror',
2934
					$wgLang->formatNum( $this->kblength ),
2935
					$wgLang->formatNum( $wgMaxArticleSize )
2936
				]
2937
			);
2938
		} else {
2939
			if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2940
				$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2941
					[
2942
						'longpage-hint',
2943
						$wgLang->formatSize( strlen( $this->textbox1 ) ),
2944
						strlen( $this->textbox1 )
2945
					]
2946
				);
2947
			}
2948
		}
2949
		# Add header copyright warning
2950
		$this->showHeaderCopyrightWarning();
2951
2952
		return true;
2953
	}
2954
2955
	/**
2956
	 * Standard summary input and label (wgSummary), abstracted so EditPage
2957
	 * subclasses may reorganize the form.
2958
	 * Note that you do not need to worry about the label's for=, it will be
2959
	 * inferred by the id given to the input. You can remove them both by
2960
	 * passing array( 'id' => false ) to $userInputAttrs.
2961
	 *
2962
	 * @param string $summary The value of the summary input
2963
	 * @param string $labelText The html to place inside the label
2964
	 * @param array $inputAttrs Array of attrs to use on the input
2965
	 * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
2966
	 *
2967
	 * @return array An array in the format array( $label, $input )
2968
	 */
2969
	function getSummaryInput( $summary = "", $labelText = null,
2970
		$inputAttrs = null, $spanLabelAttrs = null
2971
	) {
2972
		// Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2973
		$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2974
			'id' => 'wpSummary',
2975
			'maxlength' => '200',
2976
			'tabindex' => '1',
2977
			'size' => 60,
2978
			'spellcheck' => 'true',
2979
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2980
2981
		$spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
2982
			'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2983
			'id' => "wpSummaryLabel"
2984
		];
2985
2986
		$label = null;
2987
		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...
2988
			$label = Xml::tags(
2989
				'label',
2990
				$inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
2991
				$labelText
2992
			);
2993
			$label = Xml::tags( 'span', $spanLabelAttrs, $label );
2994
		}
2995
2996
		$input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2997
2998
		return [ $label, $input ];
2999
	}
3000
3001
	/**
3002
	 * @param bool $isSubjectPreview True if this is the section subject/title
3003
	 *   up top, or false if this is the comment summary
3004
	 *   down below the textarea
3005
	 * @param string $summary The text of the summary to display
3006
	 */
3007
	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3008
		global $wgOut, $wgContLang;
3009
		# Add a class if 'missingsummary' is triggered to allow styling of the summary line
3010
		$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3011
		if ( $isSubjectPreview ) {
3012
			if ( $this->nosummary ) {
3013
				return;
3014
			}
3015
		} else {
3016
			if ( !$this->mShowSummaryField ) {
3017
				return;
3018
			}
3019
		}
3020
		$summary = $wgContLang->recodeForEdit( $summary );
3021
		$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3022
		list( $label, $input ) = $this->getSummaryInput(
3023
			$summary,
3024
			$labelText,
3025
			[ 'class' => $summaryClass ],
3026
			[]
3027
		);
3028
		$wgOut->addHTML( "{$label} {$input}" );
3029
	}
3030
3031
	/**
3032
	 * @param bool $isSubjectPreview True if this is the section subject/title
3033
	 *   up top, or false if this is the comment summary
3034
	 *   down below the textarea
3035
	 * @param string $summary The text of the summary to display
3036
	 * @return string
3037
	 */
3038
	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3039
		// avoid spaces in preview, gets always trimmed on save
3040
		$summary = trim( $summary );
3041
		if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3042
			return "";
3043
		}
3044
3045
		global $wgParser;
3046
3047
		if ( $isSubjectPreview ) {
3048
			$summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3049
				->inContentLanguage()->text();
3050
		}
3051
3052
		$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3053
3054
		$summary = wfMessage( $message )->parse()
3055
			. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3056
		return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3057
	}
3058
3059
	protected function showFormBeforeText() {
3060
		global $wgOut;
3061
		$section = htmlspecialchars( $this->section );
3062
		$wgOut->addHTML( <<<HTML
3063
<input type='hidden' value="{$section}" name="wpSection"/>
3064
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3065
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3066
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3067
3068
HTML
3069
		);
3070
		if ( !$this->checkUnicodeCompliantBrowser() ) {
3071
			$wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3072
		}
3073
	}
3074
3075
	protected function showFormAfterText() {
3076
		global $wgOut, $wgUser;
3077
		/**
3078
		 * To make it harder for someone to slip a user a page
3079
		 * which submits an edit form to the wiki without their
3080
		 * knowledge, a random token is associated with the login
3081
		 * session. If it's not passed back with the submission,
3082
		 * we won't save the page, or render user JavaScript and
3083
		 * CSS previews.
3084
		 *
3085
		 * For anon editors, who may not have a session, we just
3086
		 * include the constant suffix to prevent editing from
3087
		 * broken text-mangling proxies.
3088
		 */
3089
		$wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3090
	}
3091
3092
	/**
3093
	 * Subpage overridable method for printing the form for page content editing
3094
	 * By default this simply outputs wpTextbox1
3095
	 * Subclasses can override this to provide a custom UI for editing;
3096
	 * be it a form, or simply wpTextbox1 with a modified content that will be
3097
	 * reverse modified when extracted from the post data.
3098
	 * Note that this is basically the inverse for importContentFormData
3099
	 */
3100
	protected function showContentForm() {
3101
		$this->showTextbox1();
3102
	}
3103
3104
	/**
3105
	 * Method to output wpTextbox1
3106
	 * The $textoverride method can be used by subclasses overriding showContentForm
3107
	 * to pass back to this method.
3108
	 *
3109
	 * @param array $customAttribs Array of html attributes to use in the textarea
3110
	 * @param string $textoverride Optional text to override $this->textarea1 with
3111
	 */
3112
	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3113
		if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3114
			$attribs = [ 'style' => 'display:none;' ];
3115
		} else {
3116
			$classes = []; // Textarea CSS
3117
			if ( $this->mTitle->isProtected( 'edit' ) &&
3118
				MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3119
			) {
3120
				# Is the title semi-protected?
3121
				if ( $this->mTitle->isSemiProtected() ) {
3122
					$classes[] = 'mw-textarea-sprotected';
3123
				} else {
3124
					# Then it must be protected based on static groups (regular)
3125
					$classes[] = 'mw-textarea-protected';
3126
				}
3127
				# Is the title cascade-protected?
3128
				if ( $this->mTitle->isCascadeProtected() ) {
3129
					$classes[] = 'mw-textarea-cprotected';
3130
				}
3131
			}
3132
3133
			$attribs = [ 'tabindex' => 1 ];
3134
3135
			if ( is_array( $customAttribs ) ) {
3136
				$attribs += $customAttribs;
3137
			}
3138
3139
			if ( count( $classes ) ) {
3140
				if ( isset( $attribs['class'] ) ) {
3141
					$classes[] = $attribs['class'];
3142
				}
3143
				$attribs['class'] = implode( ' ', $classes );
3144
			}
3145
		}
3146
3147
		$this->showTextbox(
3148
			$textoverride !== null ? $textoverride : $this->textbox1,
3149
			'wpTextbox1',
3150
			$attribs
3151
		);
3152
	}
3153
3154
	protected function showTextbox2() {
3155
		$this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3156
	}
3157
3158
	protected function showTextbox( $text, $name, $customAttribs = [] ) {
3159
		global $wgOut, $wgUser;
3160
3161
		$wikitext = $this->safeUnicodeOutput( $text );
3162
		if ( strval( $wikitext ) !== '' ) {
3163
			// Ensure there's a newline at the end, otherwise adding lines
3164
			// is awkward.
3165
			// But don't add a newline if the ext is empty, or Firefox in XHTML
3166
			// mode will show an extra newline. A bit annoying.
3167
			$wikitext .= "\n";
3168
		}
3169
3170
		$attribs = $customAttribs + [
3171
			'accesskey' => ',',
3172
			'id' => $name,
3173
			'cols' => $wgUser->getIntOption( 'cols' ),
3174
			'rows' => $wgUser->getIntOption( 'rows' ),
3175
			// Avoid PHP notices when appending preferences
3176
			// (appending allows customAttribs['style'] to still work).
3177
			'style' => ''
3178
		];
3179
3180
		$pageLang = $this->mTitle->getPageLanguage();
3181
		$attribs['lang'] = $pageLang->getHtmlCode();
3182
		$attribs['dir'] = $pageLang->getDir();
3183
3184
		$wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3185
	}
3186
3187
	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3188
		global $wgOut;
3189
		$classes = [];
3190
		if ( $isOnTop ) {
3191
			$classes[] = 'ontop';
3192
		}
3193
3194
		$attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3195
3196
		if ( $this->formtype != 'preview' ) {
3197
			$attribs['style'] = 'display: none;';
3198
		}
3199
3200
		$wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3201
3202
		if ( $this->formtype == 'preview' ) {
3203
			$this->showPreview( $previewOutput );
3204
		} else {
3205
			// Empty content container for LivePreview
3206
			$pageViewLang = $this->mTitle->getPageViewLanguage();
3207
			$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3208
				'class' => 'mw-content-' . $pageViewLang->getDir() ];
3209
			$wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3210
		}
3211
3212
		$wgOut->addHTML( '</div>' );
3213
3214 View Code Duplication
		if ( $this->formtype == 'diff' ) {
3215
			try {
3216
				$this->showDiff();
3217
			} catch ( MWContentSerializationException $ex ) {
3218
				$msg = wfMessage(
3219
					'content-failed-to-parse',
3220
					$this->contentModel,
3221
					$this->contentFormat,
3222
					$ex->getMessage()
3223
				);
3224
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3225
			}
3226
		}
3227
	}
3228
3229
	/**
3230
	 * Append preview output to $wgOut.
3231
	 * Includes category rendering if this is a category page.
3232
	 *
3233
	 * @param string $text The HTML to be output for the preview.
3234
	 */
3235
	protected function showPreview( $text ) {
3236
		global $wgOut;
3237
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3238
			$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...
3239
		}
3240
		# This hook seems slightly odd here, but makes things more
3241
		# consistent for extensions.
3242
		Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3243
		$wgOut->addHTML( $text );
3244
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3245
			$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...
3246
		}
3247
	}
3248
3249
	/**
3250
	 * Get a diff between the current contents of the edit box and the
3251
	 * version of the page we're editing from.
3252
	 *
3253
	 * If this is a section edit, we'll replace the section as for final
3254
	 * save and then make a comparison.
3255
	 */
3256
	function showDiff() {
3257
		global $wgUser, $wgContLang, $wgOut;
3258
3259
		$oldtitlemsg = 'currentrev';
3260
		# if message does not exist, show diff against the preloaded default
3261
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3262
			$oldtext = $this->mTitle->getDefaultMessageText();
3263
			if ( $oldtext !== false ) {
3264
				$oldtitlemsg = 'defaultmessagetext';
3265
				$oldContent = $this->toEditContent( $oldtext );
3266
			} else {
3267
				$oldContent = null;
3268
			}
3269
		} else {
3270
			$oldContent = $this->getCurrentContent();
3271
		}
3272
3273
		$textboxContent = $this->toEditContent( $this->textbox1 );
3274
3275
		$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...
3276
							$this->section, $textboxContent,
0 ignored issues
show
Bug introduced by
It seems like $textboxContent defined by $this->toEditContent($this->textbox1) on line 3273 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...
3277
							$this->summary, $this->edittime );
3278
3279
		if ( $newContent ) {
3280
			ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3281
			Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3282
3283
			$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3284
			$newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3285
		}
3286
3287
		if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3288
			$oldtitle = wfMessage( $oldtitlemsg )->parse();
3289
			$newtitle = wfMessage( 'yourtext' )->parse();
3290
3291
			if ( !$oldContent ) {
3292
				$oldContent = $newContent->getContentHandler()->makeEmptyContent();
3293
			}
3294
3295
			if ( !$newContent ) {
3296
				$newContent = $oldContent->getContentHandler()->makeEmptyContent();
3297
			}
3298
3299
			$de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3300
			$de->setContent( $oldContent, $newContent );
3301
3302
			$difftext = $de->getDiff( $oldtitle, $newtitle );
3303
			$de->showDiffStyle();
3304
		} else {
3305
			$difftext = '';
3306
		}
3307
3308
		$wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3309
	}
3310
3311
	/**
3312
	 * Show the header copyright warning.
3313
	 */
3314
	protected function showHeaderCopyrightWarning() {
3315
		$msg = 'editpage-head-copy-warn';
3316
		if ( !wfMessage( $msg )->isDisabled() ) {
3317
			global $wgOut;
3318
			$wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3319
				'editpage-head-copy-warn' );
3320
		}
3321
	}
3322
3323
	/**
3324
	 * Give a chance for site and per-namespace customizations of
3325
	 * terms of service summary link that might exist separately
3326
	 * from the copyright notice.
3327
	 *
3328
	 * This will display between the save button and the edit tools,
3329
	 * so should remain short!
3330
	 */
3331
	protected function showTosSummary() {
3332
		$msg = 'editpage-tos-summary';
3333
		Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3334
		if ( !wfMessage( $msg )->isDisabled() ) {
3335
			global $wgOut;
3336
			$wgOut->addHTML( '<div class="mw-tos-summary">' );
3337
			$wgOut->addWikiMsg( $msg );
3338
			$wgOut->addHTML( '</div>' );
3339
		}
3340
	}
3341
3342
	protected function showEditTools() {
3343
		global $wgOut;
3344
		$wgOut->addHTML( '<div class="mw-editTools">' .
3345
			wfMessage( 'edittools' )->inContentLanguage()->parse() .
3346
			'</div>' );
3347
	}
3348
3349
	/**
3350
	 * Get the copyright warning
3351
	 *
3352
	 * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3353
	 * @return string
3354
	 */
3355
	protected function getCopywarn() {
3356
		return self::getCopyrightWarning( $this->mTitle );
3357
	}
3358
3359
	/**
3360
	 * Get the copyright warning, by default returns wikitext
3361
	 *
3362
	 * @param Title $title
3363
	 * @param string $format Output format, valid values are any function of a Message object
3364
	 * @return string
3365
	 */
3366
	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3367
		global $wgRightsText;
3368
		if ( $wgRightsText ) {
3369
			$copywarnMsg = [ 'copyrightwarning',
3370
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3371
				$wgRightsText ];
3372
		} else {
3373
			$copywarnMsg = [ 'copyrightwarning2',
3374
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3375
		}
3376
		// Allow for site and per-namespace customization of contribution/copyright notice.
3377
		Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3378
3379
		return "<div id=\"editpage-copywarn\">\n" .
3380
			call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3381
	}
3382
3383
	/**
3384
	 * Get the Limit report for page previews
3385
	 *
3386
	 * @since 1.22
3387
	 * @param ParserOutput $output ParserOutput object from the parse
3388
	 * @return string HTML
3389
	 */
3390
	public static function getPreviewLimitReport( $output ) {
3391
		if ( !$output || !$output->getLimitReportData() ) {
3392
			return '';
3393
		}
3394
3395
		$limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3396
			wfMessage( 'limitreport-title' )->parseAsBlock()
3397
		);
3398
3399
		// Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3400
		$limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3401
3402
		$limitReport .= Html::openElement( 'table', [
3403
			'class' => 'preview-limit-report wikitable'
3404
		] ) .
3405
			Html::openElement( 'tbody' );
3406
3407
		foreach ( $output->getLimitReportData() as $key => $value ) {
3408
			if ( Hooks::run( 'ParserLimitReportFormat',
3409
				[ $key, &$value, &$limitReport, true, true ]
3410
			) ) {
3411
				$keyMsg = wfMessage( $key );
3412
				$valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3413
				if ( !$valueMsg->exists() ) {
3414
					$valueMsg = new RawMessage( '$1' );
3415
				}
3416
				if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3417
					$limitReport .= Html::openElement( 'tr' ) .
3418
						Html::rawElement( 'th', null, $keyMsg->parse() ) .
3419
						Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3420
						Html::closeElement( 'tr' );
3421
				}
3422
			}
3423
		}
3424
3425
		$limitReport .= Html::closeElement( 'tbody' ) .
3426
			Html::closeElement( 'table' ) .
3427
			Html::closeElement( 'div' );
3428
3429
		return $limitReport;
3430
	}
3431
3432
	protected function showStandardInputs( &$tabindex = 2 ) {
3433
		global $wgOut;
3434
		$wgOut->addHTML( "<div class='editOptions'>\n" );
3435
3436 View Code Duplication
		if ( $this->section != 'new' ) {
3437
			$this->showSummaryInput( false, $this->summary );
3438
			$wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3439
		}
3440
3441
		$checkboxes = $this->getCheckboxes( $tabindex,
3442
			[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3443
		$wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3444
3445
		// Show copyright warning.
3446
		$wgOut->addWikiText( $this->getCopywarn() );
3447
		$wgOut->addHTML( $this->editFormTextAfterWarn );
3448
3449
		$wgOut->addHTML( "<div class='editButtons'>\n" );
3450
		$wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3451
3452
		$cancel = $this->getCancelLink();
3453
		if ( $cancel !== '' ) {
3454
			$cancel .= Html::element( 'span',
3455
				[ 'class' => 'mw-editButtons-pipe-separator' ],
3456
				wfMessage( 'pipe-separator' )->text() );
3457
		}
3458
3459
		$message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3460
		$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3461
		$attrs = [
3462
			'target' => 'helpwindow',
3463
			'href' => $edithelpurl,
3464
		];
3465
		$edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3466
			$attrs, [ 'mw-ui-quiet' ] ) .
3467
			wfMessage( 'word-separator' )->escaped() .
3468
			wfMessage( 'newwindow' )->parse();
3469
3470
		$wgOut->addHTML( "	<span class='cancelLink'>{$cancel}</span>\n" );
3471
		$wgOut->addHTML( "	<span class='editHelp'>{$edithelp}</span>\n" );
3472
		$wgOut->addHTML( "</div><!-- editButtons -->\n" );
3473
3474
		Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3475
3476
		$wgOut->addHTML( "</div><!-- editOptions -->\n" );
3477
	}
3478
3479
	/**
3480
	 * Show an edit conflict. textbox1 is already shown in showEditForm().
3481
	 * If you want to use another entry point to this function, be careful.
3482
	 */
3483
	protected function showConflict() {
3484
		global $wgOut;
3485
3486
		if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3487
			$stats = $wgOut->getContext()->getStats();
3488
			$stats->increment( 'edit.failures.conflict' );
3489
3490
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3491
3492
			$content1 = $this->toEditContent( $this->textbox1 );
3493
			$content2 = $this->toEditContent( $this->textbox2 );
3494
3495
			$handler = ContentHandler::getForModelID( $this->contentModel );
3496
			$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3497
			$de->setContent( $content2, $content1 );
0 ignored issues
show
Bug introduced by
It seems like $content2 defined by $this->toEditContent($this->textbox2) on line 3493 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 3492 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...
3498
			$de->showDiff(
3499
				wfMessage( 'yourtext' )->parse(),
3500
				wfMessage( 'storedversion' )->text()
3501
			);
3502
3503
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3504
			$this->showTextbox2();
3505
		}
3506
	}
3507
3508
	/**
3509
	 * @return string
3510
	 */
3511
	public function getCancelLink() {
3512
		$cancelParams = [];
3513
		if ( !$this->isConflict && $this->oldid > 0 ) {
3514
			$cancelParams['oldid'] = $this->oldid;
3515
		} elseif ( $this->getContextTitle()->isRedirect() ) {
3516
			$cancelParams['redirect'] = 'no';
3517
		}
3518
		$attrs = [ 'id' => 'mw-editform-cancel' ];
3519
3520
		return Linker::linkKnown(
3521
			$this->getContextTitle(),
3522
			wfMessage( 'cancel' )->parse(),
3523
			Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3524
			$cancelParams
3525
		);
3526
	}
3527
3528
	/**
3529
	 * Returns the URL to use in the form's action attribute.
3530
	 * This is used by EditPage subclasses when simply customizing the action
3531
	 * variable in the constructor is not enough. This can be used when the
3532
	 * EditPage lives inside of a Special page rather than a custom page action.
3533
	 *
3534
	 * @param Title $title Title object for which is being edited (where we go to for &action= links)
3535
	 * @return string
3536
	 */
3537
	protected function getActionURL( Title $title ) {
3538
		return $title->getLocalURL( [ 'action' => $this->action ] );
3539
	}
3540
3541
	/**
3542
	 * Check if a page was deleted while the user was editing it, before submit.
3543
	 * Note that we rely on the logging table, which hasn't been always there,
3544
	 * but that doesn't matter, because this only applies to brand new
3545
	 * deletes.
3546
	 * @return bool
3547
	 */
3548
	protected function wasDeletedSinceLastEdit() {
3549
		if ( $this->deletedSinceEdit !== null ) {
3550
			return $this->deletedSinceEdit;
3551
		}
3552
3553
		$this->deletedSinceEdit = false;
3554
3555
		if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3556
			$this->lastDelete = $this->getLastDelete();
3557
			if ( $this->lastDelete ) {
3558
				$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3559
				if ( $deleteTime > $this->starttime ) {
3560
					$this->deletedSinceEdit = true;
3561
				}
3562
			}
3563
		}
3564
3565
		return $this->deletedSinceEdit;
3566
	}
3567
3568
	/**
3569
	 * @return bool|stdClass
3570
	 */
3571
	protected function getLastDelete() {
3572
		$dbr = wfGetDB( DB_SLAVE );
3573
		$data = $dbr->selectRow(
3574
			[ 'logging', 'user' ],
3575
			[
3576
				'log_type',
3577
				'log_action',
3578
				'log_timestamp',
3579
				'log_user',
3580
				'log_namespace',
3581
				'log_title',
3582
				'log_comment',
3583
				'log_params',
3584
				'log_deleted',
3585
				'user_name'
3586
			], [
3587
				'log_namespace' => $this->mTitle->getNamespace(),
3588
				'log_title' => $this->mTitle->getDBkey(),
3589
				'log_type' => 'delete',
3590
				'log_action' => 'delete',
3591
				'user_id=log_user'
3592
			],
3593
			__METHOD__,
3594
			[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3595
		);
3596
		// Quick paranoid permission checks...
3597
		if ( is_object( $data ) ) {
3598
			if ( $data->log_deleted & LogPage::DELETED_USER ) {
3599
				$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3600
			}
3601
3602
			if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3603
				$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3604
			}
3605
		}
3606
3607
		return $data;
3608
	}
3609
3610
	/**
3611
	 * Get the rendered text for previewing.
3612
	 * @throws MWException
3613
	 * @return string
3614
	 */
3615
	function getPreviewText() {
3616
		global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3617
		global $wgAllowUserCss, $wgAllowUserJs;
3618
3619
		$stats = $wgOut->getContext()->getStats();
3620
3621
		if ( $wgRawHtml && !$this->mTokenOk ) {
3622
			// Could be an offsite preview attempt. This is very unsafe if
3623
			// HTML is enabled, as it could be an attack.
3624
			$parsedNote = '';
3625
			if ( $this->textbox1 !== '' ) {
3626
				// Do not put big scary notice, if previewing the empty
3627
				// string, which happens when you initially edit
3628
				// a category page, due to automatic preview-on-open.
3629
				$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3630
					wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3631
			}
3632
			$stats->increment( 'edit.failures.session_loss' );
3633
			return $parsedNote;
3634
		}
3635
3636
		$note = '';
3637
3638
		try {
3639
			$content = $this->toEditContent( $this->textbox1 );
3640
3641
			$previewHTML = '';
3642
			if ( !Hooks::run(
3643
				'AlternateEditPreview',
3644
				[ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3645
			) {
3646
				return $previewHTML;
3647
			}
3648
3649
			# provide a anchor link to the editform
3650
			$continueEditing = '<span class="mw-continue-editing">' .
3651
				'[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3652
				wfMessage( 'continue-editing' )->text() . ']]</span>';
3653
			if ( $this->mTriedSave && !$this->mTokenOk ) {
3654
				if ( $this->mTokenOkExceptSuffix ) {
3655
					$note = wfMessage( 'token_suffix_mismatch' )->plain();
3656
					$stats->increment( 'edit.failures.bad_token' );
3657
				} else {
3658
					$note = wfMessage( 'session_fail_preview' )->plain();
3659
					$stats->increment( 'edit.failures.session_loss' );
3660
				}
3661
			} elseif ( $this->incompleteForm ) {
3662
				$note = wfMessage( 'edit_form_incomplete' )->plain();
3663
				if ( $this->mTriedSave ) {
3664
					$stats->increment( 'edit.failures.incomplete_form' );
3665
				}
3666
			} else {
3667
				$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3668
			}
3669
3670
			$parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3671
			$parserOptions->setIsPreview( true );
3672
			$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3673
3674
			# don't parse non-wikitext pages, show message about preview
3675
			if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3676
				if ( $this->mTitle->isCssJsSubpage() ) {
3677
					$level = 'user';
3678
				} elseif ( $this->mTitle->isCssOrJsPage() ) {
3679
					$level = 'site';
3680
				} else {
3681
					$level = false;
3682
				}
3683
3684
				if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3685
					$format = 'css';
3686
					if ( $level === 'user' && !$wgAllowUserCss ) {
3687
						$format = false;
3688
					}
3689
				} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3690
					$format = 'js';
3691
					if ( $level === 'user' && !$wgAllowUserJs ) {
3692
						$format = false;
3693
					}
3694
				} else {
3695
					$format = false;
3696
				}
3697
3698
				# Used messages to make sure grep find them:
3699
				# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3700
				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...
3701
					$note = "<div id='mw-{$level}{$format}preview'>" .
3702
						wfMessage( "{$level}{$format}preview" )->text() .
3703
						' ' . $continueEditing . "</div>";
3704
				}
3705
			}
3706
3707
			# If we're adding a comment, we need to show the
3708
			# summary as the headline
3709
			if ( $this->section === "new" && $this->summary !== "" ) {
3710
				$content = $content->addSectionHeader( $this->summary );
3711
			}
3712
3713
			$hook_args = [ $this, &$content ];
3714
			ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3715
			Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3716
3717
			$parserOptions->enableLimitReport();
3718
3719
			# For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3720
			# But it's now deprecated, so never mind
3721
3722
			$pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3723
			$scopedCallback = $parserOptions->setupFakeRevision(
3724
				$this->mTitle, $pstContent, $wgUser );
3725
			$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3726
3727
			$parserOutput->setEditSectionTokens( false ); // no section edit links
3728
			$previewHTML = $parserOutput->getText();
3729
			$this->mParserOutput = $parserOutput;
3730
			$wgOut->addParserOutputMetadata( $parserOutput );
3731
3732
			if ( count( $parserOutput->getWarnings() ) ) {
3733
				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3734
			}
3735
3736
			ScopedCallback::consume( $scopedCallback );
3737
		} catch ( MWContentSerializationException $ex ) {
3738
			$m = wfMessage(
3739
				'content-failed-to-parse',
3740
				$this->contentModel,
3741
				$this->contentFormat,
3742
				$ex->getMessage()
3743
			);
3744
			$note .= "\n\n" . $m->parse();
3745
			$previewHTML = '';
3746
		}
3747
3748
		if ( $this->isConflict ) {
3749
			$conflict = '<h2 id="mw-previewconflict">'
3750
				. wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3751
		} else {
3752
			$conflict = '<hr />';
3753
		}
3754
3755
		$previewhead = "<div class='previewnote'>\n" .
3756
			'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3757
			$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3758
3759
		$pageViewLang = $this->mTitle->getPageViewLanguage();
3760
		$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3761
			'class' => 'mw-content-' . $pageViewLang->getDir() ];
3762
		$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3763
3764
		return $previewhead . $previewHTML . $this->previewTextAfterContent;
3765
	}
3766
3767
	/**
3768
	 * @return array
3769
	 */
3770
	function getTemplates() {
3771
		if ( $this->preview || $this->section != '' ) {
3772
			$templates = [];
3773
			if ( !isset( $this->mParserOutput ) ) {
3774
				return $templates;
3775
			}
3776
			foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3777
				foreach ( array_keys( $template ) as $dbk ) {
3778
					$templates[] = Title::makeTitle( $ns, $dbk );
3779
				}
3780
			}
3781
			return $templates;
3782
		} else {
3783
			return $this->mTitle->getTemplateLinksFrom();
3784
		}
3785
	}
3786
3787
	/**
3788
	 * Shows a bulletin board style toolbar for common editing functions.
3789
	 * It can be disabled in the user preferences.
3790
	 *
3791
	 * @param Title $title Title object for the page being edited (optional)
3792
	 * @return string
3793
	 */
3794
	static function getEditToolbar( $title = null ) {
3795
		global $wgContLang, $wgOut;
3796
		global $wgEnableUploads, $wgForeignFileRepos;
3797
3798
		$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3799
		$showSignature = true;
3800
		if ( $title ) {
3801
			$showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3802
		}
3803
3804
		/**
3805
		 * $toolarray is an array of arrays each of which includes the
3806
		 * opening tag, the closing tag, optionally a sample text that is
3807
		 * inserted between the two when no selection is highlighted
3808
		 * and.  The tip text is shown when the user moves the mouse
3809
		 * over the button.
3810
		 *
3811
		 * Images are defined in ResourceLoaderEditToolbarModule.
3812
		 */
3813
		$toolarray = [
3814
			[
3815
				'id'     => 'mw-editbutton-bold',
3816
				'open'   => '\'\'\'',
3817
				'close'  => '\'\'\'',
3818
				'sample' => wfMessage( 'bold_sample' )->text(),
3819
				'tip'    => wfMessage( 'bold_tip' )->text(),
3820
			],
3821
			[
3822
				'id'     => 'mw-editbutton-italic',
3823
				'open'   => '\'\'',
3824
				'close'  => '\'\'',
3825
				'sample' => wfMessage( 'italic_sample' )->text(),
3826
				'tip'    => wfMessage( 'italic_tip' )->text(),
3827
			],
3828
			[
3829
				'id'     => 'mw-editbutton-link',
3830
				'open'   => '[[',
3831
				'close'  => ']]',
3832
				'sample' => wfMessage( 'link_sample' )->text(),
3833
				'tip'    => wfMessage( 'link_tip' )->text(),
3834
			],
3835
			[
3836
				'id'     => 'mw-editbutton-extlink',
3837
				'open'   => '[',
3838
				'close'  => ']',
3839
				'sample' => wfMessage( 'extlink_sample' )->text(),
3840
				'tip'    => wfMessage( 'extlink_tip' )->text(),
3841
			],
3842
			[
3843
				'id'     => 'mw-editbutton-headline',
3844
				'open'   => "\n== ",
3845
				'close'  => " ==\n",
3846
				'sample' => wfMessage( 'headline_sample' )->text(),
3847
				'tip'    => wfMessage( 'headline_tip' )->text(),
3848
			],
3849
			$imagesAvailable ? [
3850
				'id'     => 'mw-editbutton-image',
3851
				'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3852
				'close'  => ']]',
3853
				'sample' => wfMessage( 'image_sample' )->text(),
3854
				'tip'    => wfMessage( 'image_tip' )->text(),
3855
			] : false,
3856
			$imagesAvailable ? [
3857
				'id'     => 'mw-editbutton-media',
3858
				'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3859
				'close'  => ']]',
3860
				'sample' => wfMessage( 'media_sample' )->text(),
3861
				'tip'    => wfMessage( 'media_tip' )->text(),
3862
			] : false,
3863
			[
3864
				'id'     => 'mw-editbutton-nowiki',
3865
				'open'   => "<nowiki>",
3866
				'close'  => "</nowiki>",
3867
				'sample' => wfMessage( 'nowiki_sample' )->text(),
3868
				'tip'    => wfMessage( 'nowiki_tip' )->text(),
3869
			],
3870
			$showSignature ? [
3871
				'id'     => 'mw-editbutton-signature',
3872
				'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
3873
				'close'  => '',
3874
				'sample' => '',
3875
				'tip'    => wfMessage( 'sig_tip' )->text(),
3876
			] : false,
3877
			[
3878
				'id'     => 'mw-editbutton-hr',
3879
				'open'   => "\n----\n",
3880
				'close'  => '',
3881
				'sample' => '',
3882
				'tip'    => wfMessage( 'hr_tip' )->text(),
3883
			]
3884
		];
3885
3886
		$script = 'mw.loader.using("mediawiki.toolbar", function () {';
3887
		foreach ( $toolarray as $tool ) {
3888
			if ( !$tool ) {
3889
				continue;
3890
			}
3891
3892
			$params = [
3893
				// Images are defined in ResourceLoaderEditToolbarModule
3894
				false,
3895
				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3896
				// Older browsers show a "speedtip" type message only for ALT.
3897
				// Ideally these should be different, realistically they
3898
				// probably don't need to be.
3899
				$tool['tip'],
3900
				$tool['open'],
3901
				$tool['close'],
3902
				$tool['sample'],
3903
				$tool['id'],
3904
			];
3905
3906
			$script .= Xml::encodeJsCall(
3907
				'mw.toolbar.addButton',
3908
				$params,
3909
				ResourceLoader::inDebugMode()
3910
			);
3911
		}
3912
3913
		$script .= '});';
3914
		$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3915
3916
		$toolbar = '<div id="toolbar"></div>';
3917
3918
		Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3919
3920
		return $toolbar;
3921
	}
3922
3923
	/**
3924
	 * Returns an array of html code of the following checkboxes:
3925
	 * minor and watch
3926
	 *
3927
	 * @param int $tabindex Current tabindex
3928
	 * @param array $checked Array of checkbox => bool, where bool indicates the checked
3929
	 *                 status of the checkbox
3930
	 *
3931
	 * @return array
3932
	 */
3933
	public function getCheckboxes( &$tabindex, $checked ) {
3934
		global $wgUser, $wgUseMediaWikiUIEverywhere;
3935
3936
		$checkboxes = [];
3937
3938
		// don't show the minor edit checkbox if it's a new page or section
3939
		if ( !$this->isNew ) {
3940
			$checkboxes['minor'] = '';
3941
			$minorLabel = wfMessage( 'minoredit' )->parse();
3942 View Code Duplication
			if ( $wgUser->isAllowed( 'minoredit' ) ) {
3943
				$attribs = [
3944
					'tabindex' => ++$tabindex,
3945
					'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3946
					'id' => 'wpMinoredit',
3947
				];
3948
				$minorEditHtml =
3949
					Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3950
					"&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3951
					Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3952
					">{$minorLabel}</label>";
3953
3954
				if ( $wgUseMediaWikiUIEverywhere ) {
3955
					$checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3956
						$minorEditHtml .
3957
					Html::closeElement( 'div' );
3958
				} else {
3959
					$checkboxes['minor'] = $minorEditHtml;
3960
				}
3961
			}
3962
		}
3963
3964
		$watchLabel = wfMessage( 'watchthis' )->parse();
3965
		$checkboxes['watch'] = '';
3966 View Code Duplication
		if ( $wgUser->isLoggedIn() ) {
3967
			$attribs = [
3968
				'tabindex' => ++$tabindex,
3969
				'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3970
				'id' => 'wpWatchthis',
3971
			];
3972
			$watchThisHtml =
3973
				Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3974
				"&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3975
				Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
3976
				">{$watchLabel}</label>";
3977
			if ( $wgUseMediaWikiUIEverywhere ) {
3978
				$checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3979
					$watchThisHtml .
3980
					Html::closeElement( 'div' );
3981
			} else {
3982
				$checkboxes['watch'] = $watchThisHtml;
3983
			}
3984
		}
3985
		Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
3986
		return $checkboxes;
3987
	}
3988
3989
	/**
3990
	 * Returns an array of html code of the following buttons:
3991
	 * save, diff, preview and live
3992
	 *
3993
	 * @param int $tabindex Current tabindex
3994
	 *
3995
	 * @return array
3996
	 */
3997
	public function getEditButtons( &$tabindex ) {
3998
		$buttons = [];
3999
4000
		$attribs = [
4001
			'id' => 'wpSave',
4002
			'name' => 'wpSave',
4003
			'tabindex' => ++$tabindex,
4004
		] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4005
		$buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
4006
			$attribs, [ 'mw-ui-constructive' ] );
4007
4008
		++$tabindex; // use the same for preview and live preview
4009
		$attribs = [
4010
			'id' => 'wpPreview',
4011
			'name' => 'wpPreview',
4012
			'tabindex' => $tabindex,
4013
		] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4014
		$buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4015
			$attribs );
4016
		$buttons['live'] = '';
4017
4018
		$attribs = [
4019
			'id' => 'wpDiff',
4020
			'name' => 'wpDiff',
4021
			'tabindex' => ++$tabindex,
4022
		] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4023
		$buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4024
			$attribs );
4025
4026
		Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4027
		return $buttons;
4028
	}
4029
4030
	/**
4031
	 * Creates a basic error page which informs the user that
4032
	 * they have attempted to edit a nonexistent section.
4033
	 */
4034
	function noSuchSectionPage() {
4035
		global $wgOut;
4036
4037
		$wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4038
4039
		$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4040
		Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4041
		$wgOut->addHTML( $res );
4042
4043
		$wgOut->returnToMain( false, $this->mTitle );
4044
	}
4045
4046
	/**
4047
	 * Show "your edit contains spam" page with your diff and text
4048
	 *
4049
	 * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
4050
	 */
4051
	public function spamPageWithContent( $match = false ) {
4052
		global $wgOut, $wgLang;
4053
		$this->textbox2 = $this->textbox1;
4054
4055
		if ( is_array( $match ) ) {
4056
			$match = $wgLang->listToText( $match );
4057
		}
4058
		$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4059
4060
		$wgOut->addHTML( '<div id="spamprotected">' );
4061
		$wgOut->addWikiMsg( 'spamprotectiontext' );
4062
		if ( $match ) {
4063
			$wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4064
		}
4065
		$wgOut->addHTML( '</div>' );
4066
4067
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4068
		$this->showDiff();
4069
4070
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4071
		$this->showTextbox2();
4072
4073
		$wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4074
	}
4075
4076
	/**
4077
	 * Check if the browser is on a blacklist of user-agents known to
4078
	 * mangle UTF-8 data on form submission. Returns true if Unicode
4079
	 * should make it through, false if it's known to be a problem.
4080
	 * @return bool
4081
	 */
4082
	private function checkUnicodeCompliantBrowser() {
4083
		global $wgBrowserBlackList, $wgRequest;
4084
4085
		$currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4086
		if ( $currentbrowser === false ) {
4087
			// No User-Agent header sent? Trust it by default...
4088
			return true;
4089
		}
4090
4091
		foreach ( $wgBrowserBlackList as $browser ) {
4092
			if ( preg_match( $browser, $currentbrowser ) ) {
4093
				return false;
4094
			}
4095
		}
4096
		return true;
4097
	}
4098
4099
	/**
4100
	 * Filter an input field through a Unicode de-armoring process if it
4101
	 * came from an old browser with known broken Unicode editing issues.
4102
	 *
4103
	 * @param WebRequest $request
4104
	 * @param string $field
4105
	 * @return string
4106
	 */
4107
	protected function safeUnicodeInput( $request, $field ) {
4108
		$text = rtrim( $request->getText( $field ) );
4109
		return $request->getBool( 'safemode' )
4110
			? $this->unmakeSafe( $text )
4111
			: $text;
4112
	}
4113
4114
	/**
4115
	 * Filter an output field through a Unicode armoring process if it is
4116
	 * going to an old browser with known broken Unicode editing issues.
4117
	 *
4118
	 * @param string $text
4119
	 * @return string
4120
	 */
4121
	protected function safeUnicodeOutput( $text ) {
4122
		global $wgContLang;
4123
		$codedText = $wgContLang->recodeForEdit( $text );
4124
		return $this->checkUnicodeCompliantBrowser()
4125
			? $codedText
4126
			: $this->makeSafe( $codedText );
4127
	}
4128
4129
	/**
4130
	 * A number of web browsers are known to corrupt non-ASCII characters
4131
	 * in a UTF-8 text editing environment. To protect against this,
4132
	 * detected browsers will be served an armored version of the text,
4133
	 * with non-ASCII chars converted to numeric HTML character references.
4134
	 *
4135
	 * Preexisting such character references will have a 0 added to them
4136
	 * to ensure that round-trips do not alter the original data.
4137
	 *
4138
	 * @param string $invalue
4139
	 * @return string
4140
	 */
4141
	private function makeSafe( $invalue ) {
4142
		// Armor existing references for reversibility.
4143
		$invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4144
4145
		$bytesleft = 0;
4146
		$result = "";
4147
		$working = 0;
4148
		$valueLength = strlen( $invalue );
4149
		for ( $i = 0; $i < $valueLength; $i++ ) {
4150
			$bytevalue = ord( $invalue[$i] );
4151
			if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4152
				$result .= chr( $bytevalue );
4153
				$bytesleft = 0;
4154
			} elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4155
				$working = $working << 6;
4156
				$working += ( $bytevalue & 0x3F );
4157
				$bytesleft--;
4158
				if ( $bytesleft <= 0 ) {
4159
					$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4160
				}
4161
			} elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4162
				$working = $bytevalue & 0x1F;
4163
				$bytesleft = 1;
4164
			} elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4165
				$working = $bytevalue & 0x0F;
4166
				$bytesleft = 2;
4167
			} else { // 1111 0xxx
4168
				$working = $bytevalue & 0x07;
4169
				$bytesleft = 3;
4170
			}
4171
		}
4172
		return $result;
4173
	}
4174
4175
	/**
4176
	 * Reverse the previously applied transliteration of non-ASCII characters
4177
	 * back to UTF-8. Used to protect data from corruption by broken web browsers
4178
	 * as listed in $wgBrowserBlackList.
4179
	 *
4180
	 * @param string $invalue
4181
	 * @return string
4182
	 */
4183
	private function unmakeSafe( $invalue ) {
4184
		$result = "";
4185
		$valueLength = strlen( $invalue );
4186
		for ( $i = 0; $i < $valueLength; $i++ ) {
4187
			if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4188
				$i += 3;
4189
				$hexstring = "";
4190
				do {
4191
					$hexstring .= $invalue[$i];
4192
					$i++;
4193
				} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4194
4195
				// Do some sanity checks. These aren't needed for reversibility,
4196
				// but should help keep the breakage down if the editor
4197
				// breaks one of the entities whilst editing.
4198
				if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4199
					$codepoint = hexdec( $hexstring );
4200
					$result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4201
				} else {
4202
					$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4203
				}
4204
			} else {
4205
				$result .= substr( $invalue, $i, 1 );
4206
			}
4207
		}
4208
		// reverse the transform that we made for reversibility reasons.
4209
		return strtr( $result, [ "&#x0" => "&#x" ] );
4210
	}
4211
}
4212