Completed
Branch master (726f70)
by
unknown
25:29
created

EditPage::getPreviewLimitReport()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 12
rs 9.4285
c 1
b 0
f 0
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
use MediaWiki\Logger\LoggerFactory;
24
25
/**
26
 * The edit page/HTML interface (split from Article)
27
 * The actual database and text munging is still in Article,
28
 * but it should get easier to call those from alternate
29
 * interfaces.
30
 *
31
 * EditPage cares about two distinct titles:
32
 * $this->mContextTitle is the page that forms submit to, links point to,
33
 * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
34
 * page in the database that is actually being edited. These are
35
 * usually the same, but they are now allowed to be different.
36
 *
37
 * Surgeon General's Warning: prolonged exposure to this class is known to cause
38
 * headaches, which may be fatal.
39
 */
40
class EditPage {
41
	/**
42
	 * Status: Article successfully updated
43
	 */
44
	const AS_SUCCESS_UPDATE = 200;
45
46
	/**
47
	 * Status: Article successfully created
48
	 */
49
	const AS_SUCCESS_NEW_ARTICLE = 201;
50
51
	/**
52
	 * Status: Article update aborted by a hook function
53
	 */
54
	const AS_HOOK_ERROR = 210;
55
56
	/**
57
	 * Status: A hook function returned an error
58
	 */
59
	const AS_HOOK_ERROR_EXPECTED = 212;
60
61
	/**
62
	 * Status: User is blocked from editing this page
63
	 */
64
	const AS_BLOCKED_PAGE_FOR_USER = 215;
65
66
	/**
67
	 * Status: Content too big (> $wgMaxArticleSize)
68
	 */
69
	const AS_CONTENT_TOO_BIG = 216;
70
71
	/**
72
	 * Status: this anonymous user is not allowed to edit this page
73
	 */
74
	const AS_READ_ONLY_PAGE_ANON = 218;
75
76
	/**
77
	 * Status: this logged in user is not allowed to edit this page
78
	 */
79
	const AS_READ_ONLY_PAGE_LOGGED = 219;
80
81
	/**
82
	 * Status: wiki is in readonly mode (wfReadOnly() == true)
83
	 */
84
	const AS_READ_ONLY_PAGE = 220;
85
86
	/**
87
	 * Status: rate limiter for action 'edit' was tripped
88
	 */
89
	const AS_RATE_LIMITED = 221;
90
91
	/**
92
	 * Status: article was deleted while editing and param wpRecreate == false or form
93
	 * was not posted
94
	 */
95
	const AS_ARTICLE_WAS_DELETED = 222;
96
97
	/**
98
	 * Status: user tried to create this page, but is not allowed to do that
99
	 * ( Title->userCan('create') == false )
100
	 */
101
	const AS_NO_CREATE_PERMISSION = 223;
102
103
	/**
104
	 * Status: user tried to create a blank page and wpIgnoreBlankArticle == false
105
	 */
106
	const AS_BLANK_ARTICLE = 224;
107
108
	/**
109
	 * Status: (non-resolvable) edit conflict
110
	 */
111
	const AS_CONFLICT_DETECTED = 225;
112
113
	/**
114
	 * Status: no edit summary given and the user has forceeditsummary set and the user is not
115
	 * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false
116
	 */
117
	const AS_SUMMARY_NEEDED = 226;
118
119
	/**
120
	 * Status: user tried to create a new section without content
121
	 */
122
	const AS_TEXTBOX_EMPTY = 228;
123
124
	/**
125
	 * Status: article is too big (> $wgMaxArticleSize), after merging in the new section
126
	 */
127
	const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
128
129
	/**
130
	 * Status: WikiPage::doEdit() was unsuccessful
131
	 */
132
	const AS_END = 231;
133
134
	/**
135
	 * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex
136
	 */
137
	const AS_SPAM_ERROR = 232;
138
139
	/**
140
	 * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
141
	 */
142
	const AS_IMAGE_REDIRECT_ANON = 233;
143
144
	/**
145
	 * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
146
	 */
147
	const AS_IMAGE_REDIRECT_LOGGED = 234;
148
149
	/**
150
	 * Status: user tried to modify the content model, but is not allowed to do that
151
	 * ( User::isAllowed('editcontentmodel') == false )
152
	 */
153
	const AS_NO_CHANGE_CONTENT_MODEL = 235;
154
155
	/**
156
	 * Status: user tried to create self-redirect (redirect to the same article) and
157
	 * wpIgnoreSelfRedirect == false
158
	 */
159
	const AS_SELF_REDIRECT = 236;
160
161
	/**
162
	 * Status: an error relating to change tagging. Look at the message key for
163
	 * more details
164
	 */
165
	const AS_CHANGE_TAG_ERROR = 237;
166
167
	/**
168
	 * Status: can't parse content
169
	 */
170
	const AS_PARSE_ERROR = 240;
171
172
	/**
173
	 * Status: when changing the content model is disallowed due to
174
	 * $wgContentHandlerUseDB being false
175
	 */
176
	const AS_CANNOT_USE_CUSTOM_MODEL = 241;
177
178
	/**
179
	 * HTML id and name for the beginning of the edit form.
180
	 */
181
	const EDITFORM_ID = 'editform';
182
183
	/**
184
	 * Prefix of key for cookie used to pass post-edit state.
185
	 * The revision id edited is added after this
186
	 */
187
	const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
188
189
	/**
190
	 * Duration of PostEdit cookie, in seconds.
191
	 * The cookie will be removed instantly if the JavaScript runs.
192
	 *
193
	 * Otherwise, though, we don't want the cookies to accumulate.
194
	 * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible
195
	 * limit of only 20 cookies per domain. This still applies at least to some
196
	 * versions of IE without full updates:
197
	 * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
198
	 *
199
	 * A value of 20 minutes should be enough to take into account slow loads and minor
200
	 * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
201
	 */
202
	const POST_EDIT_COOKIE_DURATION = 1200;
203
204
	/** @var Article */
205
	public $mArticle;
206
	/** @var WikiPage */
207
	private $page;
208
209
	/** @var Title */
210
	public $mTitle;
211
212
	/** @var null|Title */
213
	private $mContextTitle = null;
214
215
	/** @var string */
216
	public $action = 'submit';
217
218
	/** @var bool */
219
	public $isConflict = false;
220
221
	/** @var bool */
222
	public $isCssJsSubpage = false;
223
224
	/** @var bool */
225
	public $isCssSubpage = false;
226
227
	/** @var bool */
228
	public $isJsSubpage = false;
229
230
	/** @var bool */
231
	public $isWrongCaseCssJsPage = false;
232
233
	/** @var bool New page or new section */
234
	public $isNew = false;
235
236
	/** @var bool */
237
	public $deletedSinceEdit;
238
239
	/** @var string */
240
	public $formtype;
241
242
	/** @var bool */
243
	public $firsttime;
244
245
	/** @var bool|stdClass */
246
	public $lastDelete;
247
248
	/** @var bool */
249
	public $mTokenOk = false;
250
251
	/** @var bool */
252
	public $mTokenOkExceptSuffix = false;
253
254
	/** @var bool */
255
	public $mTriedSave = false;
256
257
	/** @var bool */
258
	public $incompleteForm = false;
259
260
	/** @var bool */
261
	public $tooBig = false;
262
263
	/** @var bool */
264
	public $missingComment = false;
265
266
	/** @var bool */
267
	public $missingSummary = false;
268
269
	/** @var bool */
270
	public $allowBlankSummary = false;
271
272
	/** @var bool */
273
	protected $blankArticle = false;
274
275
	/** @var bool */
276
	protected $allowBlankArticle = false;
277
278
	/** @var bool */
279
	protected $selfRedirect = false;
280
281
	/** @var bool */
282
	protected $allowSelfRedirect = false;
283
284
	/** @var string */
285
	public $autoSumm = '';
286
287
	/** @var string */
288
	public $hookError = '';
289
290
	/** @var ParserOutput */
291
	public $mParserOutput;
292
293
	/** @var bool Has a summary been preset using GET parameter &summary= ? */
294
	public $hasPresetSummary = false;
295
296
	/** @var bool */
297
	public $mBaseRevision = false;
298
299
	/** @var bool */
300
	public $mShowSummaryField = true;
301
302
	# Form values
303
304
	/** @var bool */
305
	public $save = false;
306
307
	/** @var bool */
308
	public $preview = false;
309
310
	/** @var bool */
311
	public $diff = false;
312
313
	/** @var bool */
314
	public $minoredit = false;
315
316
	/** @var bool */
317
	public $watchthis = false;
318
319
	/** @var bool */
320
	public $recreate = false;
321
322
	/** @var string */
323
	public $textbox1 = '';
324
325
	/** @var string */
326
	public $textbox2 = '';
327
328
	/** @var string */
329
	public $summary = '';
330
331
	/** @var bool */
332
	public $nosummary = false;
333
334
	/** @var string */
335
	public $edittime = '';
336
337
	/** @var integer */
338
	private $editRevId = null;
339
340
	/** @var string */
341
	public $section = '';
342
343
	/** @var string */
344
	public $sectiontitle = '';
345
346
	/** @var string */
347
	public $starttime = '';
348
349
	/** @var int */
350
	public $oldid = 0;
351
352
	/** @var int */
353
	public $parentRevId = 0;
354
355
	/** @var string */
356
	public $editintro = '';
357
358
	/** @var null */
359
	public $scrolltop = null;
360
361
	/** @var bool */
362
	public $bot = true;
363
364
	/** @var null|string */
365
	public $contentModel = null;
366
367
	/** @var null|string */
368
	public $contentFormat = null;
369
370
	/** @var null|array */
371
	private $changeTags = null;
372
373
	# Placeholders for text injection by hooks (must be HTML)
374
	# extensions should take care to _append_ to the present value
375
376
	/** @var string Before even the preview */
377
	public $editFormPageTop = '';
378
	public $editFormTextTop = '';
379
	public $editFormTextBeforeContent = '';
380
	public $editFormTextAfterWarn = '';
381
	public $editFormTextAfterTools = '';
382
	public $editFormTextBottom = '';
383
	public $editFormTextAfterContent = '';
384
	public $previewTextAfterContent = '';
385
	public $mPreloadContent = null;
386
387
	/* $didSave should be set to true whenever an article was successfully altered. */
388
	public $didSave = false;
389
	public $undidRev = 0;
390
391
	public $suppressIntro = false;
392
393
	/** @var bool */
394
	protected $edit;
395
396
	/** @var bool|int */
397
	protected $contentLength = false;
398
399
	/**
400
	 * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
401
	 */
402
	private $enableApiEditOverride = false;
403
404
	/**
405
	 * @param Article $article
406
	 */
407
	public function __construct( Article $article ) {
408
		$this->mArticle = $article;
409
		$this->page = $article->getPage(); // model object
410
		$this->mTitle = $article->getTitle();
411
412
		$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...
413
414
		$handler = ContentHandler::getForModelID( $this->contentModel );
415
		$this->contentFormat = $handler->getDefaultFormat();
416
	}
417
418
	/**
419
	 * @return Article
420
	 */
421
	public function getArticle() {
422
		return $this->mArticle;
423
	}
424
425
	/**
426
	 * @since 1.19
427
	 * @return Title
428
	 */
429
	public function getTitle() {
430
		return $this->mTitle;
431
	}
432
433
	/**
434
	 * Set the context Title object
435
	 *
436
	 * @param Title|null $title Title object or null
437
	 */
438
	public function setContextTitle( $title ) {
439
		$this->mContextTitle = $title;
440
	}
441
442
	/**
443
	 * Get the context title object.
444
	 * If not set, $wgTitle will be returned. This behavior might change in
445
	 * the future to return $this->mTitle instead.
446
	 *
447
	 * @return Title
448
	 */
449
	public function getContextTitle() {
450
		if ( is_null( $this->mContextTitle ) ) {
451
			global $wgTitle;
452
			return $wgTitle;
453
		} else {
454
			return $this->mContextTitle;
455
		}
456
	}
457
458
	/**
459
	 * Returns if the given content model is editable.
460
	 *
461
	 * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
462
	 * @return bool
463
	 * @throws MWException If $modelId has no known handler
464
	 */
465
	public function isSupportedContentModel( $modelId ) {
466
		return $this->enableApiEditOverride === true ||
467
			ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
468
	}
469
470
	/**
471
	 * Allow editing of content that supports API direct editing, but not general
472
	 * direct editing. Set to false by default.
473
	 *
474
	 * @param bool $enableOverride
475
	 */
476
	public function setApiEditOverride( $enableOverride ) {
477
		$this->enableApiEditOverride = $enableOverride;
478
	}
479
480
	function submit() {
481
		$this->edit();
482
	}
483
484
	/**
485
	 * This is the function that gets called for "action=edit". It
486
	 * sets up various member variables, then passes execution to
487
	 * another function, usually showEditForm()
488
	 *
489
	 * The edit form is self-submitting, so that when things like
490
	 * preview and edit conflicts occur, we get the same form back
491
	 * with the extra stuff added.  Only when the final submission
492
	 * is made and all is well do we actually save and redirect to
493
	 * the newly-edited page.
494
	 */
495
	function edit() {
496
		global $wgOut, $wgRequest, $wgUser;
497
		// Allow extensions to modify/prevent this form or submission
498
		if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
499
			return;
500
		}
501
502
		wfDebug( __METHOD__ . ": enter\n" );
503
504
		// If they used redlink=1 and the page exists, redirect to the main article
505
		if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
506
			$wgOut->redirect( $this->mTitle->getFullURL() );
507
			return;
508
		}
509
510
		$this->importFormData( $wgRequest );
511
		$this->firsttime = false;
512
513
		if ( wfReadOnly() && $this->save ) {
514
			// Force preview
515
			$this->save = false;
516
			$this->preview = true;
517
		}
518
519
		if ( $this->save ) {
520
			$this->formtype = 'save';
521
		} elseif ( $this->preview ) {
522
			$this->formtype = 'preview';
523
		} elseif ( $this->diff ) {
524
			$this->formtype = 'diff';
525
		} else { # First time through
526
			$this->firsttime = true;
527
			if ( $this->previewOnOpen() ) {
528
				$this->formtype = 'preview';
529
			} else {
530
				$this->formtype = 'initial';
531
			}
532
		}
533
534
		$permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
535
		if ( $permErrors ) {
536
			wfDebug( __METHOD__ . ": User can't edit\n" );
537
			// Auto-block user's IP if the account was "hard" blocked
538
			if ( !wfReadOnly() ) {
539
				$user = $wgUser;
540
				DeferredUpdates::addCallableUpdate( function () use ( $user ) {
541
					$user->spreadAnyEditBlock();
542
				} );
543
			}
544
			$this->displayPermissionsError( $permErrors );
545
546
			return;
547
		}
548
549
		$revision = $this->mArticle->getRevisionFetched();
550
		// Disallow editing revisions with content models different from the current one
551
		if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
552
			$this->displayViewSourcePage(
553
				$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...
554
				wfMessage(
555
					'contentmodelediterror',
556
					$revision->getContentModel(),
557
					$this->contentModel
558
				)->plain()
559
			);
560
			return;
561
		}
562
563
		$this->isConflict = false;
564
		// css / js subpages of user pages get a special treatment
565
		$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
566
		$this->isCssSubpage = $this->mTitle->isCssSubpage();
567
		$this->isJsSubpage = $this->mTitle->isJsSubpage();
568
		// @todo FIXME: Silly assignment.
569
		$this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
570
571
		# Show applicable editing introductions
572
		if ( $this->formtype == 'initial' || $this->firsttime ) {
573
			$this->showIntro();
574
		}
575
576
		# Attempt submission here.  This will check for edit conflicts,
577
		# and redundantly check for locked database, blocked IPs, etc.
578
		# that edit() already checked just in case someone tries to sneak
579
		# in the back door with a hand-edited submission URL.
580
581
		if ( 'save' == $this->formtype ) {
582
			$resultDetails = null;
583
			$status = $this->attemptSave( $resultDetails );
584
			if ( !$this->handleStatus( $status, $resultDetails ) ) {
585
				return;
586
			}
587
		}
588
589
		# First time through: get contents, set time for conflict
590
		# checking, etc.
591
		if ( 'initial' == $this->formtype || $this->firsttime ) {
592
			if ( $this->initialiseForm() === false ) {
593
				$this->noSuchSectionPage();
594
				return;
595
			}
596
597
			if ( !$this->mTitle->getArticleID() ) {
598
				Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
599
			} else {
600
				Hooks::run( 'EditFormInitialText', [ $this ] );
601
			}
602
603
		}
604
605
		$this->showEditForm();
606
	}
607
608
	/**
609
	 * @param string $rigor Same format as Title::getUserPermissionErrors()
610
	 * @return array
611
	 */
612
	protected function getEditPermissionErrors( $rigor = 'secure' ) {
613
		global $wgUser;
614
615
		$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
616
		# Can this title be created?
617
		if ( !$this->mTitle->exists() ) {
618
			$permErrors = array_merge(
619
				$permErrors,
620
				wfArrayDiff2(
621
					$this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
622
					$permErrors
623
				)
624
			);
625
		}
626
		# Ignore some permissions errors when a user is just previewing/viewing diffs
627
		$remove = [];
628
		foreach ( $permErrors as $error ) {
629
			if ( ( $this->preview || $this->diff )
630
				&& ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
631
			) {
632
				$remove[] = $error;
633
			}
634
		}
635
		$permErrors = wfArrayDiff2( $permErrors, $remove );
636
637
		return $permErrors;
638
	}
639
640
	/**
641
	 * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
642
	 * but with the following differences:
643
	 * - If redlink=1, the user will be redirected to the page
644
	 * - If there is content to display or the error occurs while either saving,
645
	 *   previewing or showing the difference, it will be a
646
	 *   "View source for ..." page displaying the source code after the error message.
647
	 *
648
	 * @since 1.19
649
	 * @param array $permErrors Array of permissions errors, as returned by
650
	 *    Title::getUserPermissionsErrors().
651
	 * @throws PermissionsError
652
	 */
653
	protected function displayPermissionsError( array $permErrors ) {
654
		global $wgRequest, $wgOut;
655
656
		if ( $wgRequest->getBool( 'redlink' ) ) {
657
			// The edit page was reached via a red link.
658
			// Redirect to the article page and let them click the edit tab if
659
			// they really want a permission error.
660
			$wgOut->redirect( $this->mTitle->getFullURL() );
661
			return;
662
		}
663
664
		$content = $this->getContentObject();
665
666
		# Use the normal message if there's nothing to display
667
		if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
668
			$action = $this->mTitle->exists() ? 'edit' :
669
				( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
670
			throw new PermissionsError( $action, $permErrors );
671
		}
672
673
		$this->displayViewSourcePage(
674
			$content,
0 ignored issues
show
Bug introduced by
It seems like $content defined by $this->getContentObject() on line 664 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...
675
			$wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
676
		);
677
	}
678
679
	/**
680
	 * Display a read-only View Source page
681
	 * @param Content $content content object
682
	 * @param string $errorMessage additional wikitext error message to display
683
	 */
684
	protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
685
		global $wgOut;
686
687
		Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
688
689
		$wgOut->setRobotPolicy( 'noindex,nofollow' );
690
		$wgOut->setPageTitle( wfMessage(
691
			'viewsource-title',
692
			$this->getContextTitle()->getPrefixedText()
693
		) );
694
		$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
695
		$wgOut->addHTML( $this->editFormPageTop );
696
		$wgOut->addHTML( $this->editFormTextTop );
697
698
		if ( $errorMessage !== '' ) {
699
			$wgOut->addWikiText( $errorMessage );
700
			$wgOut->addHTML( "<hr />\n" );
701
		}
702
703
		# If the user made changes, preserve them when showing the markup
704
		# (This happens when a user is blocked during edit, for instance)
705
		if ( !$this->firsttime ) {
706
			$text = $this->textbox1;
707
			$wgOut->addWikiMsg( 'viewyourtext' );
708
		} else {
709
			try {
710
				$text = $this->toEditText( $content );
711
			} catch ( MWException $e ) {
712
				# Serialize using the default format if the content model is not supported
713
				# (e.g. for an old revision with a different model)
714
				$text = $content->serialize();
715
			}
716
			$wgOut->addWikiMsg( 'viewsourcetext' );
717
		}
718
719
		$wgOut->addHTML( $this->editFormTextBeforeContent );
720
		$this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
721
		$wgOut->addHTML( $this->editFormTextAfterContent );
722
723
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
724
			Linker::formatTemplates( $this->getTemplates() ) ) );
725
726
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
727
728
		$wgOut->addHTML( $this->editFormTextBottom );
729
		if ( $this->mTitle->exists() ) {
730
			$wgOut->returnToMain( null, $this->mTitle );
731
		}
732
	}
733
734
	/**
735
	 * Should we show a preview when the edit form is first shown?
736
	 *
737
	 * @return bool
738
	 */
739
	protected function previewOnOpen() {
740
		global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
741
		if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
742
			// Explicit override from request
743
			return true;
744
		} elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
745
			// Explicit override from request
746
			return false;
747
		} elseif ( $this->section == 'new' ) {
748
			// Nothing *to* preview for new sections
749
			return false;
750
		} elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
751
			&& $wgUser->getOption( 'previewonfirst' )
752
		) {
753
			// Standard preference behavior
754
			return true;
755
		} elseif ( !$this->mTitle->exists()
756
			&& isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
757
			&& $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
758
		) {
759
			// Categories are special
760
			return true;
761
		} else {
762
			return false;
763
		}
764
	}
765
766
	/**
767
	 * Checks whether the user entered a skin name in uppercase,
768
	 * e.g. "User:Example/Monobook.css" instead of "monobook.css"
769
	 *
770
	 * @return bool
771
	 */
772
	protected function isWrongCaseCssJsPage() {
773
		if ( $this->mTitle->isCssJsSubpage() ) {
774
			$name = $this->mTitle->getSkinFromCssJsSubpage();
775
			$skins = array_merge(
776
				array_keys( Skin::getSkinNames() ),
777
				[ 'common' ]
778
			);
779
			return !in_array( $name, $skins )
780
				&& in_array( strtolower( $name ), $skins );
781
		} else {
782
			return false;
783
		}
784
	}
785
786
	/**
787
	 * Returns whether section editing is supported for the current page.
788
	 * Subclasses may override this to replace the default behavior, which is
789
	 * to check ContentHandler::supportsSections.
790
	 *
791
	 * @return bool True if this edit page supports sections, false otherwise.
792
	 */
793
	protected function isSectionEditSupported() {
794
		$contentHandler = ContentHandler::getForTitle( $this->mTitle );
795
		return $contentHandler->supportsSections();
796
	}
797
798
	/**
799
	 * This function collects the form data and uses it to populate various member variables.
800
	 * @param WebRequest $request
801
	 * @throws ErrorPageError
802
	 */
803
	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...
804
		global $wgContLang, $wgUser;
805
806
		# Section edit can come from either the form or a link
807
		$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
808
809
		if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
810
			throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
811
		}
812
813
		$this->isNew = !$this->mTitle->exists() || $this->section == 'new';
814
815
		if ( $request->wasPosted() ) {
816
			# These fields need to be checked for encoding.
817
			# Also remove trailing whitespace, but don't remove _initial_
818
			# whitespace from the text boxes. This may be significant formatting.
819
			$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
820
			if ( !$request->getCheck( 'wpTextbox2' ) ) {
821
				// Skip this if wpTextbox2 has input, it indicates that we came
822
				// from a conflict page with raw page text, not a custom form
823
				// modified by subclasses
824
				$textbox1 = $this->importContentFormData( $request );
825
				if ( $textbox1 !== null ) {
826
					$this->textbox1 = $textbox1;
827
				}
828
			}
829
830
			# Truncate for whole multibyte characters
831
			$this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
832
833
			# If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
834
			# header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
835
			# section titles.
836
			$this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
837
838
			# Treat sectiontitle the same way as summary.
839
			# Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
840
			# currently doing double duty as both edit summary and section title. Right now this
841
			# is just to allow API edits to work around this limitation, but this should be
842
			# incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
843
			$this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
844
			$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
845
846
			$this->edittime = $request->getVal( 'wpEdittime' );
847
			$this->editRevId = $request->getIntOrNull( 'editRevId' );
848
			$this->starttime = $request->getVal( 'wpStarttime' );
849
850
			$undidRev = $request->getInt( 'wpUndidRevision' );
851
			if ( $undidRev ) {
852
				$this->undidRev = $undidRev;
853
			}
854
855
			$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...
856
857
			if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
858
				// wpTextbox1 field is missing, possibly due to being "too big"
859
				// according to some filter rules such as Suhosin's setting for
860
				// suhosin.request.max_value_length (d'oh)
861
				$this->incompleteForm = true;
862
			} else {
863
				// If we receive the last parameter of the request, we can fairly
864
				// claim the POST request has not been truncated.
865
866
				// TODO: softened the check for cutover.  Once we determine
867
				// that it is safe, we should complete the transition by
868
				// removing the "edittime" clause.
869
				$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...
870
					&& is_null( $this->edittime ) );
871
			}
872
			if ( $this->incompleteForm ) {
873
				# If the form is incomplete, force to preview.
874
				wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
875
				wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
876
				$this->preview = true;
877
			} else {
878
				$this->preview = $request->getCheck( 'wpPreview' );
879
				$this->diff = $request->getCheck( 'wpDiff' );
880
881
				// Remember whether a save was requested, so we can indicate
882
				// if we forced preview due to session failure.
883
				$this->mTriedSave = !$this->preview;
884
885
				if ( $this->tokenOk( $request ) ) {
886
					# Some browsers will not report any submit button
887
					# if the user hits enter in the comment box.
888
					# The unmarked state will be assumed to be a save,
889
					# if the form seems otherwise complete.
890
					wfDebug( __METHOD__ . ": Passed token check.\n" );
891
				} elseif ( $this->diff ) {
892
					# Failed token check, but only requested "Show Changes".
893
					wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
894
				} else {
895
					# Page might be a hack attempt posted from
896
					# an external site. Preview instead of saving.
897
					wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
898
					$this->preview = true;
899
				}
900
			}
901
			$this->save = !$this->preview && !$this->diff;
902
			if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
903
				$this->edittime = null;
904
			}
905
906
			if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
907
				$this->starttime = null;
908
			}
909
910
			$this->recreate = $request->getCheck( 'wpRecreate' );
911
912
			$this->minoredit = $request->getCheck( 'wpMinoredit' );
913
			$this->watchthis = $request->getCheck( 'wpWatchthis' );
914
915
			# Don't force edit summaries when a user is editing their own user or talk page
916
			if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
917
				&& $this->mTitle->getText() == $wgUser->getName()
918
			) {
919
				$this->allowBlankSummary = true;
920
			} else {
921
				$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
922
					|| !$wgUser->getOption( 'forceeditsummary' );
923
			}
924
925
			$this->autoSumm = $request->getText( 'wpAutoSummary' );
926
927
			$this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
928
			$this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
929
930
			$changeTags = $request->getVal( 'wpChangeTags' );
931
			if ( is_null( $changeTags ) || $changeTags === '' ) {
932
				$this->changeTags = [];
933
			} else {
934
				$this->changeTags = array_filter( array_map( 'trim', explode( ',',
935
					$changeTags ) ) );
936
			}
937
		} else {
938
			# Not a posted form? Start with nothing.
939
			wfDebug( __METHOD__ . ": Not a posted form.\n" );
940
			$this->textbox1 = '';
941
			$this->summary = '';
942
			$this->sectiontitle = '';
943
			$this->edittime = '';
944
			$this->editRevId = null;
945
			$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...
946
			$this->edit = false;
947
			$this->preview = false;
948
			$this->save = false;
949
			$this->diff = false;
950
			$this->minoredit = false;
951
			// Watch may be overridden by request parameters
952
			$this->watchthis = $request->getBool( 'watchthis', false );
953
			$this->recreate = false;
954
955
			// When creating a new section, we can preload a section title by passing it as the
956
			// preloadtitle parameter in the URL (Bug 13100)
957
			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...
958
				$this->sectiontitle = $request->getVal( 'preloadtitle' );
959
				// Once wpSummary isn't being use for setting section titles, we should delete this.
960
				$this->summary = $request->getVal( 'preloadtitle' );
961
			} 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...
962
				$this->summary = $request->getText( 'summary' );
963
				if ( $this->summary !== '' ) {
964
					$this->hasPresetSummary = true;
965
				}
966
			}
967
968
			if ( $request->getVal( 'minor' ) ) {
969
				$this->minoredit = true;
970
			}
971
		}
972
973
		$this->oldid = $request->getInt( 'oldid' );
974
		$this->parentRevId = $request->getInt( 'parentRevId' );
975
976
		$this->bot = $request->getBool( 'bot', true );
977
		$this->nosummary = $request->getBool( 'nosummary' );
978
979
		// May be overridden by revision.
980
		$this->contentModel = $request->getText( 'model', $this->contentModel );
981
		// May be overridden by revision.
982
		$this->contentFormat = $request->getText( 'format', $this->contentFormat );
983
984
		if ( !ContentHandler::getForModelID( $this->contentModel )
985
			->isSupportedFormat( $this->contentFormat )
986
		) {
987
			throw new ErrorPageError(
988
				'editpage-notsupportedcontentformat-title',
989
				'editpage-notsupportedcontentformat-text',
990
				[ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ]
991
			);
992
		}
993
994
		/**
995
		 * @todo Check if the desired model is allowed in this namespace, and if
996
		 *   a transition from the page's current model to the new model is
997
		 *   allowed.
998
		 */
999
1000
		$this->editintro = $request->getText( 'editintro',
1001
			// Custom edit intro for new sections
1002
			$this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1003
1004
		// Allow extensions to modify form data
1005
		Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1006
1007
	}
1008
1009
	/**
1010
	 * Subpage overridable method for extracting the page content data from the
1011
	 * posted form to be placed in $this->textbox1, if using customized input
1012
	 * this method should be overridden and return the page text that will be used
1013
	 * for saving, preview parsing and so on...
1014
	 *
1015
	 * @param WebRequest $request
1016
	 * @return string|null
1017
	 */
1018
	protected function importContentFormData( &$request ) {
1019
		return; // Don't do anything, EditPage already extracted wpTextbox1
1020
	}
1021
1022
	/**
1023
	 * Initialise form fields in the object
1024
	 * Called on the first invocation, e.g. when a user clicks an edit link
1025
	 * @return bool If the requested section is valid
1026
	 */
1027
	function initialiseForm() {
1028
		global $wgUser;
1029
		$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...
1030
		$this->editRevId = $this->page->getLatest();
1031
1032
		$content = $this->getContentObject( false ); # TODO: track content object?!
1033
		if ( $content === false ) {
1034
			return false;
1035
		}
1036
		$this->textbox1 = $this->toEditText( $content );
1037
1038
		// activate checkboxes if user wants them to be always active
1039
		# Sort out the "watch" checkbox
1040
		if ( $wgUser->getOption( 'watchdefault' ) ) {
1041
			# Watch all edits
1042
			$this->watchthis = true;
1043
		} elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1044
			# Watch creations
1045
			$this->watchthis = true;
1046
		} elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1047
			# Already watched
1048
			$this->watchthis = true;
1049
		}
1050
		if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1051
			$this->minoredit = true;
1052
		}
1053
		if ( $this->textbox1 === false ) {
1054
			return false;
1055
		}
1056
		return true;
1057
	}
1058
1059
	/**
1060
	 * @param Content|null $def_content The default value to return
1061
	 *
1062
	 * @return Content|null Content on success, $def_content for invalid sections
1063
	 *
1064
	 * @since 1.21
1065
	 */
1066
	protected function getContentObject( $def_content = null ) {
1067
		global $wgOut, $wgRequest, $wgUser, $wgContLang;
1068
1069
		$content = false;
1070
1071
		// For message page not locally set, use the i18n message.
1072
		// For other non-existent articles, use preload text if any.
1073
		if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1074
			if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1075
				# If this is a system message, get the default text.
1076
				$msg = $this->mTitle->getDefaultMessageText();
1077
1078
				$content = $this->toEditContent( $msg );
1079
			}
1080
			if ( $content === false ) {
1081
				# If requested, preload some text.
1082
				$preload = $wgRequest->getVal( 'preload',
1083
					// Custom preload text for new sections
1084
					$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1085
				$params = $wgRequest->getArray( 'preloadparams', [] );
1086
1087
				$content = $this->getPreloadedContent( $preload, $params );
1088
			}
1089
		// For existing pages, get text based on "undo" or section parameters.
1090
		} else {
1091
			if ( $this->section != '' ) {
1092
				// Get section edit text (returns $def_text for invalid sections)
1093
				$orig = $this->getOriginalContent( $wgUser );
1094
				$content = $orig ? $orig->getSection( $this->section ) : null;
1095
1096
				if ( !$content ) {
1097
					$content = $def_content;
1098
				}
1099
			} else {
1100
				$undoafter = $wgRequest->getInt( 'undoafter' );
1101
				$undo = $wgRequest->getInt( 'undo' );
1102
1103
				if ( $undo > 0 && $undoafter > 0 ) {
1104
					$undorev = Revision::newFromId( $undo );
1105
					$oldrev = Revision::newFromId( $undoafter );
1106
1107
					# Sanity check, make sure it's the right page,
1108
					# the revisions exist and they were not deleted.
1109
					# Otherwise, $content will be left as-is.
1110
					if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1111
						!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1112
						!$oldrev->isDeleted( Revision::DELETED_TEXT )
1113
					) {
1114
						$content = $this->page->getUndoContent( $undorev, $oldrev );
1115
1116
						if ( $content === false ) {
1117
							# Warn the user that something went wrong
1118
							$undoMsg = 'failure';
1119
						} else {
1120
							$oldContent = $this->page->getContent( Revision::RAW );
1121
							$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1122
							$newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1123
1124
							if ( $newContent->equals( $oldContent ) ) {
1125
								# Tell the user that the undo results in no change,
1126
								# i.e. the revisions were already undone.
1127
								$undoMsg = 'nochange';
1128
								$content = false;
1129
							} else {
1130
								# Inform the user of our success and set an automatic edit summary
1131
								$undoMsg = 'success';
1132
1133
								# If we just undid one rev, use an autosummary
1134
								$firstrev = $oldrev->getNext();
1135
								if ( $firstrev && $firstrev->getId() == $undo ) {
1136
									$userText = $undorev->getUserText();
1137
									if ( $userText === '' ) {
1138
										$undoSummary = wfMessage(
1139
											'undo-summary-username-hidden',
1140
											$undo
1141
										)->inContentLanguage()->text();
1142
									} else {
1143
										$undoSummary = wfMessage(
1144
											'undo-summary',
1145
											$undo,
1146
											$userText
1147
										)->inContentLanguage()->text();
1148
									}
1149
									if ( $this->summary === '' ) {
1150
										$this->summary = $undoSummary;
1151
									} else {
1152
										$this->summary = $undoSummary . wfMessage( 'colon-separator' )
1153
											->inContentLanguage()->text() . $this->summary;
1154
									}
1155
									$this->undidRev = $undo;
1156
								}
1157
								$this->formtype = 'diff';
1158
							}
1159
						}
1160
					} else {
1161
						// Failed basic sanity checks.
1162
						// Older revisions may have been removed since the link
1163
						// was created, or we may simply have got bogus input.
1164
						$undoMsg = 'norev';
1165
					}
1166
1167
					// Messages: undo-success, undo-failure, undo-norev, undo-nochange
1168
					$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1169
					$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1170
						wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1171
				}
1172
1173
				if ( $content === false ) {
1174
					$content = $this->getOriginalContent( $wgUser );
1175
				}
1176
			}
1177
		}
1178
1179
		return $content;
1180
	}
1181
1182
	/**
1183
	 * Get the content of the wanted revision, without section extraction.
1184
	 *
1185
	 * The result of this function can be used to compare user's input with
1186
	 * section replaced in its context (using WikiPage::replaceSectionAtRev())
1187
	 * to the original text of the edit.
1188
	 *
1189
	 * This differs from Article::getContent() that when a missing revision is
1190
	 * encountered the result will be null and not the
1191
	 * 'missing-revision' message.
1192
	 *
1193
	 * @since 1.19
1194
	 * @param User $user The user to get the revision for
1195
	 * @return Content|null
1196
	 */
1197
	private function getOriginalContent( User $user ) {
1198
		if ( $this->section == 'new' ) {
1199
			return $this->getCurrentContent();
1200
		}
1201
		$revision = $this->mArticle->getRevisionFetched();
1202
		if ( $revision === null ) {
1203
			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...
1204
				$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...
1205
			}
1206
			$handler = ContentHandler::getForModelID( $this->contentModel );
1207
1208
			return $handler->makeEmptyContent();
1209
		}
1210
		$content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1211
		return $content;
1212
	}
1213
1214
	/**
1215
	 * Get the edit's parent revision ID
1216
	 *
1217
	 * The "parent" revision is the ancestor that should be recorded in this
1218
	 * page's revision history.  It is either the revision ID of the in-memory
1219
	 * article content, or in the case of a 3-way merge in order to rebase
1220
	 * across a recoverable edit conflict, the ID of the newer revision to
1221
	 * which we have rebased this page.
1222
	 *
1223
	 * @since 1.27
1224
	 * @return int Revision ID
1225
	 */
1226
	public function getParentRevId() {
1227
		if ( $this->parentRevId ) {
1228
			return $this->parentRevId;
1229
		} else {
1230
			return $this->mArticle->getRevIdFetched();
1231
		}
1232
	}
1233
1234
	/**
1235
	 * Get the current content of the page. This is basically similar to
1236
	 * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
1237
	 * content object is returned instead of null.
1238
	 *
1239
	 * @since 1.21
1240
	 * @return Content
1241
	 */
1242
	protected function getCurrentContent() {
1243
		$rev = $this->page->getRevision();
1244
		$content = $rev ? $rev->getContent( Revision::RAW ) : null;
1245
1246
		if ( $content === false || $content === null ) {
1247
			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...
1248
				$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...
1249
			}
1250
			$handler = ContentHandler::getForModelID( $this->contentModel );
1251
1252
			return $handler->makeEmptyContent();
1253
		} else {
1254
			// Content models should always be the same since we error
1255
			// out if they are different before this point.
1256
			$logger = LoggerFactory::getInstance( 'editpage' );
1257 View Code Duplication
			if ( $this->contentModel !== $rev->getContentModel() ) {
1258
				$logger->warning( "Overriding content model from current edit {prev} to {new}", [
1259
					'prev' => $this->contentModel,
1260
					'new' => $rev->getContentModel(),
1261
					'title' => $this->getTitle()->getPrefixedDBkey(),
1262
					'method' => __METHOD__
1263
				] );
1264
				$this->contentModel = $rev->getContentModel();
1265
			}
1266
1267
			// Given that the content models should match, the current selected
1268
			// format should be supported.
1269 View Code Duplication
			if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1270
				$logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1271
1272
					'prev' => $this->contentFormat,
1273
					'new' => $rev->getContentFormat(),
1274
					'title' => $this->getTitle()->getPrefixedDBkey(),
1275
					'method' => __METHOD__
1276
				] );
1277
				$this->contentFormat = $rev->getContentFormat();
1278
			}
1279
1280
			return $content;
1281
		}
1282
	}
1283
1284
	/**
1285
	 * Use this method before edit() to preload some content into the edit box
1286
	 *
1287
	 * @param Content $content
1288
	 *
1289
	 * @since 1.21
1290
	 */
1291
	public function setPreloadedContent( Content $content ) {
1292
		$this->mPreloadContent = $content;
1293
	}
1294
1295
	/**
1296
	 * Get the contents to be preloaded into the box, either set by
1297
	 * an earlier setPreloadText() or by loading the given page.
1298
	 *
1299
	 * @param string $preload Representing the title to preload from.
1300
	 * @param array $params Parameters to use (interface-message style) in the preloaded text
1301
	 *
1302
	 * @return Content
1303
	 *
1304
	 * @since 1.21
1305
	 */
1306
	protected function getPreloadedContent( $preload, $params = [] ) {
1307
		global $wgUser;
1308
1309
		if ( !empty( $this->mPreloadContent ) ) {
1310
			return $this->mPreloadContent;
1311
		}
1312
1313
		$handler = ContentHandler::getForModelID( $this->contentModel );
1314
1315
		if ( $preload === '' ) {
1316
			return $handler->makeEmptyContent();
1317
		}
1318
1319
		$title = Title::newFromText( $preload );
1320
		# Check for existence to avoid getting MediaWiki:Noarticletext
1321 View Code Duplication
		if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1322
			// TODO: somehow show a warning to the user!
1323
			return $handler->makeEmptyContent();
1324
		}
1325
1326
		$page = WikiPage::factory( $title );
1327
		if ( $page->isRedirect() ) {
1328
			$title = $page->getRedirectTarget();
1329
			# Same as before
1330 View Code Duplication
			if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1331
				// TODO: somehow show a warning to the user!
1332
				return $handler->makeEmptyContent();
1333
			}
1334
			$page = WikiPage::factory( $title );
1335
		}
1336
1337
		$parserOptions = ParserOptions::newFromUser( $wgUser );
1338
		$content = $page->getContent( Revision::RAW );
1339
1340
		if ( !$content ) {
1341
			// TODO: somehow show a warning to the user!
1342
			return $handler->makeEmptyContent();
1343
		}
1344
1345
		if ( $content->getModel() !== $handler->getModelID() ) {
1346
			$converted = $content->convert( $handler->getModelID() );
1347
1348
			if ( !$converted ) {
1349
				// TODO: somehow show a warning to the user!
1350
				wfDebug( "Attempt to preload incompatible content: " .
1351
					"can't convert " . $content->getModel() .
1352
					" to " . $handler->getModelID() );
1353
1354
				return $handler->makeEmptyContent();
1355
			}
1356
1357
			$content = $converted;
1358
		}
1359
1360
		return $content->preloadTransform( $title, $parserOptions, $params );
1361
	}
1362
1363
	/**
1364
	 * Make sure the form isn't faking a user's credentials.
1365
	 *
1366
	 * @param WebRequest $request
1367
	 * @return bool
1368
	 * @private
1369
	 */
1370
	function tokenOk( &$request ) {
1371
		global $wgUser;
1372
		$token = $request->getVal( 'wpEditToken' );
1373
		$this->mTokenOk = $wgUser->matchEditToken( $token );
1374
		$this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1375
		return $this->mTokenOk;
1376
	}
1377
1378
	/**
1379
	 * Sets post-edit cookie indicating the user just saved a particular revision.
1380
	 *
1381
	 * This uses a temporary cookie for each revision ID so separate saves will never
1382
	 * interfere with each other.
1383
	 *
1384
	 * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
1385
	 * the redirect.  It must be clearable by JavaScript code, so it must not be
1386
	 * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
1387
	 * variable.
1388
	 *
1389
	 * If the variable were set on the server, it would be cached, which is unwanted
1390
	 * since the post-edit state should only apply to the load right after the save.
1391
	 *
1392
	 * @param int $statusValue The status value (to check for new article status)
1393
	 */
1394
	protected function setPostEditCookie( $statusValue ) {
1395
		$revisionId = $this->page->getLatest();
1396
		$postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1397
1398
		$val = 'saved';
1399
		if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1400
			$val = 'created';
1401
		} elseif ( $this->oldid ) {
1402
			$val = 'restored';
1403
		}
1404
1405
		$response = RequestContext::getMain()->getRequest()->response();
1406
		$response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1407
			'httpOnly' => false,
1408
		] );
1409
	}
1410
1411
	/**
1412
	 * Attempt submission
1413
	 * @param array $resultDetails See docs for $result in internalAttemptSave
1414
	 * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
1415
	 * @return Status The resulting status object.
1416
	 */
1417
	public function attemptSave( &$resultDetails = false ) {
1418
		global $wgUser;
1419
1420
		# Allow bots to exempt some edits from bot flagging
1421
		$bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1422
		$status = $this->internalAttemptSave( $resultDetails, $bot );
0 ignored issues
show
Bug introduced by
It seems like $resultDetails defined by parameter $resultDetails on line 1417 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...
1423
1424
		Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1425
1426
		return $status;
1427
	}
1428
1429
	/**
1430
	 * Handle status, such as after attempt save
1431
	 *
1432
	 * @param Status $status
1433
	 * @param array|bool $resultDetails
1434
	 *
1435
	 * @throws ErrorPageError
1436
	 * @return bool False, if output is done, true if rest of the form should be displayed
1437
	 */
1438
	private function handleStatus( Status $status, $resultDetails ) {
1439
		global $wgUser, $wgOut;
1440
1441
		/**
1442
		 * @todo FIXME: once the interface for internalAttemptSave() is made
1443
		 *   nicer, this should use the message in $status
1444
		 */
1445
		if ( $status->value == self::AS_SUCCESS_UPDATE
1446
			|| $status->value == self::AS_SUCCESS_NEW_ARTICLE
1447
		) {
1448
			$this->didSave = true;
1449
			if ( !$resultDetails['nullEdit'] ) {
1450
				$this->setPostEditCookie( $status->value );
1451
			}
1452
		}
1453
1454
		// "wpExtraQueryRedirect" is a hidden input to modify
1455
		// after save URL and is not used by actual edit form
1456
		$request = RequestContext::getMain()->getRequest();
1457
		$extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1458
1459
		switch ( $status->value ) {
1460
			case self::AS_HOOK_ERROR_EXPECTED:
1461
			case self::AS_CONTENT_TOO_BIG:
1462
			case self::AS_ARTICLE_WAS_DELETED:
1463
			case self::AS_CONFLICT_DETECTED:
1464
			case self::AS_SUMMARY_NEEDED:
1465
			case self::AS_TEXTBOX_EMPTY:
1466
			case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1467
			case self::AS_END:
1468
			case self::AS_BLANK_ARTICLE:
1469
			case self::AS_SELF_REDIRECT:
1470
				return true;
1471
1472
			case self::AS_HOOK_ERROR:
1473
				return false;
1474
1475
			case self::AS_CANNOT_USE_CUSTOM_MODEL:
1476
			case self::AS_PARSE_ERROR:
1477
				$wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1478
				return true;
1479
1480
			case self::AS_SUCCESS_NEW_ARTICLE:
1481
				$query = $resultDetails['redirect'] ? 'redirect=no' : '';
1482
				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...
1483
					if ( $query === '' ) {
1484
						$query = $extraQueryRedirect;
1485
					} else {
1486
						$query = $query . '&' . $extraQueryRedirect;
1487
					}
1488
				}
1489
				$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1490
				$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1491
				return false;
1492
1493
			case self::AS_SUCCESS_UPDATE:
1494
				$extraQuery = '';
1495
				$sectionanchor = $resultDetails['sectionanchor'];
1496
1497
				// Give extensions a chance to modify URL query on update
1498
				Hooks::run(
1499
					'ArticleUpdateBeforeRedirect',
1500
					[ $this->mArticle, &$sectionanchor, &$extraQuery ]
1501
				);
1502
1503
				if ( $resultDetails['redirect'] ) {
1504
					if ( $extraQuery == '' ) {
1505
						$extraQuery = 'redirect=no';
1506
					} else {
1507
						$extraQuery = 'redirect=no&' . $extraQuery;
1508
					}
1509
				}
1510
				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...
1511
					if ( $extraQuery === '' ) {
1512
						$extraQuery = $extraQueryRedirect;
1513
					} else {
1514
						$extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1515
					}
1516
				}
1517
1518
				$wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1519
				return false;
1520
1521
			case self::AS_SPAM_ERROR:
1522
				$this->spamPageWithContent( $resultDetails['spam'] );
1523
				return false;
1524
1525
			case self::AS_BLOCKED_PAGE_FOR_USER:
1526
				throw new UserBlockedError( $wgUser->getBlock() );
1527
1528
			case self::AS_IMAGE_REDIRECT_ANON:
1529
			case self::AS_IMAGE_REDIRECT_LOGGED:
1530
				throw new PermissionsError( 'upload' );
1531
1532
			case self::AS_READ_ONLY_PAGE_ANON:
1533
			case self::AS_READ_ONLY_PAGE_LOGGED:
1534
				throw new PermissionsError( 'edit' );
1535
1536
			case self::AS_READ_ONLY_PAGE:
1537
				throw new ReadOnlyError;
1538
1539
			case self::AS_RATE_LIMITED:
1540
				throw new ThrottledError();
1541
1542
			case self::AS_NO_CREATE_PERMISSION:
1543
				$permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1544
				throw new PermissionsError( $permission );
1545
1546
			case self::AS_NO_CHANGE_CONTENT_MODEL:
1547
				throw new PermissionsError( 'editcontentmodel' );
1548
1549
			default:
1550
				// We don't recognize $status->value. The only way that can happen
1551
				// is if an extension hook aborted from inside ArticleSave.
1552
				// Render the status object into $this->hookError
1553
				// FIXME this sucks, we should just use the Status object throughout
1554
				$this->hookError = '<div class="error">' . $status->getWikiText() .
1555
					'</div>';
1556
				return true;
1557
		}
1558
	}
1559
1560
	/**
1561
	 * Run hooks that can filter edits just before they get saved.
1562
	 *
1563
	 * @param Content $content The Content to filter.
1564
	 * @param Status $status For reporting the outcome to the caller
1565
	 * @param User $user The user performing the edit
1566
	 *
1567
	 * @return bool
1568
	 */
1569
	protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1570
		// Run old style post-section-merge edit filter
1571
		if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1572
			[ $this, $content, &$this->hookError, $this->summary ] )
1573
		) {
1574
			# Error messages etc. could be handled within the hook...
1575
			$status->fatal( 'hookaborted' );
1576
			$status->value = self::AS_HOOK_ERROR;
1577
			return false;
1578
		} elseif ( $this->hookError != '' ) {
1579
			# ...or the hook could be expecting us to produce an error
1580
			$status->fatal( 'hookaborted' );
1581
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1582
			return false;
1583
		}
1584
1585
		// Run new style post-section-merge edit filter
1586
		if ( !Hooks::run( 'EditFilterMergedContent',
1587
				[ $this->mArticle->getContext(), $content, $status, $this->summary,
1588
				$user, $this->minoredit ] )
1589
		) {
1590
			# Error messages etc. could be handled within the hook...
1591
			if ( $status->isGood() ) {
1592
				$status->fatal( 'hookaborted' );
1593
				// Not setting $this->hookError here is a hack to allow the hook
1594
				// to cause a return to the edit page without $this->hookError
1595
				// being set. This is used by ConfirmEdit to display a captcha
1596
				// without any error message cruft.
1597
			} else {
1598
				$this->hookError = $status->getWikiText();
1599
			}
1600
			// Use the existing $status->value if the hook set it
1601
			if ( !$status->value ) {
1602
				$status->value = self::AS_HOOK_ERROR;
1603
			}
1604
			return false;
1605
		} elseif ( !$status->isOK() ) {
1606
			# ...or the hook could be expecting us to produce an error
1607
			// FIXME this sucks, we should just use the Status object throughout
1608
			$this->hookError = $status->getWikiText();
1609
			$status->fatal( 'hookaborted' );
1610
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1611
			return false;
1612
		}
1613
1614
		return true;
1615
	}
1616
1617
	/**
1618
	 * Return the summary to be used for a new section.
1619
	 *
1620
	 * @param string $sectionanchor Set to the section anchor text
1621
	 * @return string
1622
	 */
1623
	private function newSectionSummary( &$sectionanchor = null ) {
1624
		global $wgParser;
1625
1626
		if ( $this->sectiontitle !== '' ) {
1627
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1628
			// If no edit summary was specified, create one automatically from the section
1629
			// title and have it link to the new section. Otherwise, respect the summary as
1630
			// passed.
1631
			if ( $this->summary === '' ) {
1632
				$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1633
				return wfMessage( 'newsectionsummary' )
1634
					->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1635
			}
1636
		} elseif ( $this->summary !== '' ) {
1637
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1638
			# This is a new section, so create a link to the new section
1639
			# in the revision summary.
1640
			$cleanSummary = $wgParser->stripSectionName( $this->summary );
1641
			return wfMessage( 'newsectionsummary' )
1642
				->rawParams( $cleanSummary )->inContentLanguage()->text();
1643
		}
1644
		return $this->summary;
1645
	}
1646
1647
	/**
1648
	 * Attempt submission (no UI)
1649
	 *
1650
	 * @param array $result Array to add statuses to, currently with the
1651
	 *   possible keys:
1652
	 *   - spam (string): Spam string from content if any spam is detected by
1653
	 *     matchSpamRegex.
1654
	 *   - sectionanchor (string): Section anchor for a section save.
1655
	 *   - nullEdit (boolean): Set if doEditContent is OK.  True if null edit,
1656
	 *     false otherwise.
1657
	 *   - redirect (bool): Set if doEditContent is OK. True if resulting
1658
	 *     revision is a redirect.
1659
	 * @param bool $bot True if edit is being made under the bot right.
1660
	 *
1661
	 * @return Status Status object, possibly with a message, but always with
1662
	 *   one of the AS_* constants in $status->value,
1663
	 *
1664
	 * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to
1665
	 *   various error display idiosyncrasies. There are also lots of cases
1666
	 *   where error metadata is set in the object and retrieved later instead
1667
	 *   of being returned, e.g. AS_CONTENT_TOO_BIG and
1668
	 *   AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some
1669
	 * time.
1670
	 */
1671
	function internalAttemptSave( &$result, $bot = false ) {
1672
		global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
1673
		global $wgContentHandlerUseDB;
1674
1675
		$status = Status::newGood();
1676
1677
		if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1678
			wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1679
			$status->fatal( 'hookaborted' );
1680
			$status->value = self::AS_HOOK_ERROR;
1681
			return $status;
1682
		}
1683
1684
		$spam = $wgRequest->getText( 'wpAntispam' );
1685
		if ( $spam !== '' ) {
1686
			wfDebugLog(
1687
				'SimpleAntiSpam',
1688
				$wgUser->getName() .
1689
				' editing "' .
1690
				$this->mTitle->getPrefixedText() .
1691
				'" submitted bogus field "' .
1692
				$spam .
1693
				'"'
1694
			);
1695
			$status->fatal( 'spamprotectionmatch', false );
1696
			$status->value = self::AS_SPAM_ERROR;
1697
			return $status;
1698
		}
1699
1700
		try {
1701
			# Construct Content object
1702
			$textbox_content = $this->toEditContent( $this->textbox1 );
1703
		} catch ( MWContentSerializationException $ex ) {
1704
			$status->fatal(
1705
				'content-failed-to-parse',
1706
				$this->contentModel,
1707
				$this->contentFormat,
1708
				$ex->getMessage()
1709
			);
1710
			$status->value = self::AS_PARSE_ERROR;
1711
			return $status;
1712
		}
1713
1714
		# Check image redirect
1715
		if ( $this->mTitle->getNamespace() == NS_FILE &&
1716
			$textbox_content->isRedirect() &&
1717
			!$wgUser->isAllowed( 'upload' )
1718
		) {
1719
				$code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1720
				$status->setResult( false, $code );
1721
1722
				return $status;
1723
		}
1724
1725
		# Check for spam
1726
		$match = self::matchSummarySpamRegex( $this->summary );
1727
		if ( $match === false && $this->section == 'new' ) {
1728
			# $wgSpamRegex is enforced on this new heading/summary because, unlike
1729
			# regular summaries, it is added to the actual wikitext.
1730
			if ( $this->sectiontitle !== '' ) {
1731
				# This branch is taken when the API is used with the 'sectiontitle' parameter.
1732
				$match = self::matchSpamRegex( $this->sectiontitle );
1733
			} else {
1734
				# This branch is taken when the "Add Topic" user interface is used, or the API
1735
				# is used with the 'summary' parameter.
1736
				$match = self::matchSpamRegex( $this->summary );
1737
			}
1738
		}
1739
		if ( $match === false ) {
1740
			$match = self::matchSpamRegex( $this->textbox1 );
1741
		}
1742
		if ( $match !== false ) {
1743
			$result['spam'] = $match;
1744
			$ip = $wgRequest->getIP();
1745
			$pdbk = $this->mTitle->getPrefixedDBkey();
1746
			$match = str_replace( "\n", '', $match );
1747
			wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1748
			$status->fatal( 'spamprotectionmatch', $match );
1749
			$status->value = self::AS_SPAM_ERROR;
1750
			return $status;
1751
		}
1752
		if ( !Hooks::run(
1753
			'EditFilter',
1754
			[ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1755
		) {
1756
			# Error messages etc. could be handled within the hook...
1757
			$status->fatal( 'hookaborted' );
1758
			$status->value = self::AS_HOOK_ERROR;
1759
			return $status;
1760
		} elseif ( $this->hookError != '' ) {
1761
			# ...or the hook could be expecting us to produce an error
1762
			$status->fatal( 'hookaborted' );
1763
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1764
			return $status;
1765
		}
1766
1767
		if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1768
			// Auto-block user's IP if the account was "hard" blocked
1769
			if ( !wfReadOnly() ) {
1770
				$wgUser->spreadAnyEditBlock();
1771
			}
1772
			# Check block state against master, thus 'false'.
1773
			$status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1774
			return $status;
1775
		}
1776
1777
		$this->contentLength = strlen( $this->textbox1 );
1778 View Code Duplication
		if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
1779
			// Error will be displayed by showEditForm()
1780
			$this->tooBig = true;
1781
			$status->setResult( false, self::AS_CONTENT_TOO_BIG );
1782
			return $status;
1783
		}
1784
1785 View Code Duplication
		if ( !$wgUser->isAllowed( 'edit' ) ) {
1786
			if ( $wgUser->isAnon() ) {
1787
				$status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1788
				return $status;
1789
			} else {
1790
				$status->fatal( 'readonlytext' );
1791
				$status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1792
				return $status;
1793
			}
1794
		}
1795
1796
		$changingContentModel = false;
1797
		if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1798 View Code Duplication
			if ( !$wgContentHandlerUseDB ) {
1799
				$status->fatal( 'editpage-cannot-use-custom-model' );
1800
				$status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1801
				return $status;
1802
			} elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1803
				$status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1804
				return $status;
1805
1806
			}
1807
			$changingContentModel = true;
1808
			$oldContentModel = $this->mTitle->getContentModel();
1809
		}
1810
1811
		if ( $this->changeTags ) {
1812
			$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1813
				$this->changeTags, $wgUser );
1814
			if ( !$changeTagsStatus->isOK() ) {
1815
				$changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1816
				return $changeTagsStatus;
1817
			}
1818
		}
1819
1820
		if ( wfReadOnly() ) {
1821
			$status->fatal( 'readonlytext' );
1822
			$status->value = self::AS_READ_ONLY_PAGE;
1823
			return $status;
1824
		}
1825
		if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1826
			$status->fatal( 'actionthrottledtext' );
1827
			$status->value = self::AS_RATE_LIMITED;
1828
			return $status;
1829
		}
1830
1831
		# If the article has been deleted while editing, don't save it without
1832
		# confirmation
1833
		if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1834
			$status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1835
			return $status;
1836
		}
1837
1838
		# Load the page data from the master. If anything changes in the meantime,
1839
		# we detect it by using page_latest like a token in a 1 try compare-and-swap.
1840
		$this->page->loadPageData( 'fromdbmaster' );
1841
		$new = !$this->page->exists();
1842
1843
		if ( $new ) {
1844
			// Late check for create permission, just in case *PARANOIA*
1845
			if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1846
				$status->fatal( 'nocreatetext' );
1847
				$status->value = self::AS_NO_CREATE_PERMISSION;
1848
				wfDebug( __METHOD__ . ": no create permission\n" );
1849
				return $status;
1850
			}
1851
1852
			// Don't save a new page if it's blank or if it's a MediaWiki:
1853
			// message with content equivalent to default (allow empty pages
1854
			// in this case to disable messages, see bug 50124)
1855
			$defaultMessageText = $this->mTitle->getDefaultMessageText();
1856
			if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1857
				$defaultText = $defaultMessageText;
1858
			} else {
1859
				$defaultText = '';
1860
			}
1861
1862
			if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1863
				$this->blankArticle = true;
1864
				$status->fatal( 'blankarticle' );
1865
				$status->setResult( false, self::AS_BLANK_ARTICLE );
1866
				return $status;
1867
			}
1868
1869
			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 1702 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...
1870
				return $status;
1871
			}
1872
1873
			$content = $textbox_content;
1874
1875
			$result['sectionanchor'] = '';
1876
			if ( $this->section == 'new' ) {
1877
				if ( $this->sectiontitle !== '' ) {
1878
					// Insert the section title above the content.
1879
					$content = $content->addSectionHeader( $this->sectiontitle );
1880
				} elseif ( $this->summary !== '' ) {
1881
					// Insert the section title above the content.
1882
					$content = $content->addSectionHeader( $this->summary );
1883
				}
1884
				$this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1885
			}
1886
1887
			$status->value = self::AS_SUCCESS_NEW_ARTICLE;
1888
1889
		} else { # not $new
1890
1891
			# Article exists. Check for edit conflict.
1892
1893
			$this->page->clear(); # Force reload of dates, etc.
1894
			$timestamp = $this->page->getTimestamp();
1895
			$latest = $this->page->getLatest();
1896
1897
			wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1898
1899
			// Check editRevId if set, which handles same-second timestamp collisions
1900
			if ( $timestamp != $this->edittime
1901
				|| ( $this->editRevId !== null && $this->editRevId != $latest )
1902
			) {
1903
				$this->isConflict = true;
1904
				if ( $this->section == 'new' ) {
1905
					if ( $this->page->getUserText() == $wgUser->getName() &&
1906
						$this->page->getComment() == $this->newSectionSummary()
1907
					) {
1908
						// Probably a duplicate submission of a new comment.
1909
						// This can happen when CDN resends a request after
1910
						// a timeout but the first one actually went through.
1911
						wfDebug( __METHOD__
1912
							. ": duplicate new section submission; trigger edit conflict!\n" );
1913
					} else {
1914
						// New comment; suppress conflict.
1915
						$this->isConflict = false;
1916
						wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1917
					}
1918
				} elseif ( $this->section == ''
1919
					&& Revision::userWasLastToEdit(
1920
						DB_MASTER, $this->mTitle->getArticleID(),
1921
						$wgUser->getId(), $this->edittime
1922
					)
1923
				) {
1924
					# Suppress edit conflict with self, except for section edits where merging is required.
1925
					wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1926
					$this->isConflict = false;
1927
				}
1928
			}
1929
1930
			// If sectiontitle is set, use it, otherwise use the summary as the section title.
1931
			if ( $this->sectiontitle !== '' ) {
1932
				$sectionTitle = $this->sectiontitle;
1933
			} else {
1934
				$sectionTitle = $this->summary;
1935
			}
1936
1937
			$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...
1938
1939
			if ( $this->isConflict ) {
1940
				wfDebug( __METHOD__
1941
					. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1942
					. " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
1943
				// @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
1944
				// ...or disable section editing for non-current revisions (not exposed anyway).
1945
				if ( $this->editRevId !== null ) {
1946
					$content = $this->page->replaceSectionAtRev(
1947
						$this->section,
1948
						$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1702 can be null; however, WikiPage::replaceSectionAtRev() 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...
1949
						$sectionTitle,
1950
						$this->editRevId
1951
					);
1952
				} else {
1953
					$content = $this->page->replaceSectionContent(
1954
						$this->section,
1955
						$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1702 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...
1956
						$sectionTitle,
1957
						$this->edittime
1958
					);
1959
				}
1960
			} else {
1961
				wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1962
				$content = $this->page->replaceSectionContent(
1963
					$this->section,
1964
					$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1702 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...
1965
					$sectionTitle
1966
				);
1967
			}
1968
1969
			if ( is_null( $content ) ) {
1970
				wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1971
				$this->isConflict = true;
1972
				$content = $textbox_content; // do not try to merge here!
1973
			} elseif ( $this->isConflict ) {
1974
				# Attempt merge
1975
				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...
1976
					// Successful merge! Maybe we should tell the user the good news?
1977
					$this->isConflict = false;
1978
					wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1979
				} else {
1980
					$this->section = '';
1981
					$this->textbox1 = ContentHandler::getContentText( $content );
1982
					wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1983
				}
1984
			}
1985
1986
			if ( $this->isConflict ) {
1987
				$status->setResult( false, self::AS_CONFLICT_DETECTED );
1988
				return $status;
1989
			}
1990
1991
			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...
1992
				return $status;
1993
			}
1994
1995
			if ( $this->section == 'new' ) {
1996
				// Handle the user preference to force summaries here
1997
				if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1998
					$this->missingSummary = true;
1999
					$status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2000
					$status->value = self::AS_SUMMARY_NEEDED;
2001
					return $status;
2002
				}
2003
2004
				// Do not allow the user to post an empty comment
2005
				if ( $this->textbox1 == '' ) {
2006
					$this->missingComment = true;
2007
					$status->fatal( 'missingcommenttext' );
2008
					$status->value = self::AS_TEXTBOX_EMPTY;
2009
					return $status;
2010
				}
2011
			} elseif ( !$this->allowBlankSummary
2012
				&& !$content->equals( $this->getOriginalContent( $wgUser ) )
2013
				&& !$content->isRedirect()
2014
				&& md5( $this->summary ) == $this->autoSumm
2015
			) {
2016
				$this->missingSummary = true;
2017
				$status->fatal( 'missingsummary' );
2018
				$status->value = self::AS_SUMMARY_NEEDED;
2019
				return $status;
2020
			}
2021
2022
			# All's well
2023
			$sectionanchor = '';
2024
			if ( $this->section == 'new' ) {
2025
				$this->summary = $this->newSectionSummary( $sectionanchor );
2026
			} elseif ( $this->section != '' ) {
2027
				# Try to get a section anchor from the section source, redirect
2028
				# to edited section if header found.
2029
				# XXX: Might be better to integrate this into Article::replaceSectionAtRev
2030
				# for duplicate heading checking and maybe parsing.
2031
				$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2032
				# We can't deal with anchors, includes, html etc in the header for now,
2033
				# headline would need to be parsed to improve this.
2034
				if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2035
					$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
2036
				}
2037
			}
2038
			$result['sectionanchor'] = $sectionanchor;
2039
2040
			// Save errors may fall down to the edit form, but we've now
2041
			// merged the section into full text. Clear the section field
2042
			// so that later submission of conflict forms won't try to
2043
			// replace that into a duplicated mess.
2044
			$this->textbox1 = $this->toEditText( $content );
2045
			$this->section = '';
2046
2047
			$status->value = self::AS_SUCCESS_UPDATE;
2048
		}
2049
2050
		if ( !$this->allowSelfRedirect
2051
			&& $content->isRedirect()
2052
			&& $content->getRedirectTarget()->equals( $this->getTitle() )
2053
		) {
2054
			// If the page already redirects to itself, don't warn.
2055
			$currentTarget = $this->getCurrentContent()->getRedirectTarget();
2056
			if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2057
				$this->selfRedirect = true;
2058
				$status->fatal( 'selfredirect' );
2059
				$status->value = self::AS_SELF_REDIRECT;
2060
				return $status;
2061
			}
2062
		}
2063
2064
		// Check for length errors again now that the section is merged in
2065
		$this->contentLength = strlen( $this->toEditText( $content ) );
2066 View Code Duplication
		if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
2067
			$this->tooBig = true;
2068
			$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2069
			return $status;
2070
		}
2071
2072
		$flags = EDIT_AUTOSUMMARY |
2073
			( $new ? EDIT_NEW : EDIT_UPDATE ) |
2074
			( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2075
			( $bot ? EDIT_FORCE_BOT : 0 );
2076
2077
		$doEditStatus = $this->page->doEditContent(
2078
			$content,
2079
			$this->summary,
2080
			$flags,
2081
			false,
2082
			$wgUser,
2083
			$content->getDefaultFormat(),
2084
			$this->changeTags
2085
		);
2086
2087
		if ( !$doEditStatus->isOK() ) {
2088
			// Failure from doEdit()
2089
			// Show the edit conflict page for certain recognized errors from doEdit(),
2090
			// but don't show it for errors from extension hooks
2091
			$errors = $doEditStatus->getErrorsArray();
2092
			if ( in_array( $errors[0][0],
2093
					[ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2094
			) {
2095
				$this->isConflict = true;
2096
				// Destroys data doEdit() put in $status->value but who cares
2097
				$doEditStatus->value = self::AS_END;
2098
			}
2099
			return $doEditStatus;
2100
		}
2101
2102
		$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2103
		if ( $result['nullEdit'] ) {
2104
			// We don't know if it was a null edit until now, so increment here
2105
			$wgUser->pingLimiter( 'linkpurge' );
2106
		}
2107
		$result['redirect'] = $content->isRedirect();
2108
2109
		$this->updateWatchlist();
2110
2111
		// If the content model changed, add a log entry
2112
		if ( $changingContentModel ) {
2113
			$this->addContentModelChangeLogEntry(
2114
				$wgUser,
2115
				$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...
2116
				$this->contentModel,
2117
				$this->summary
2118
			);
2119
		}
2120
2121
		return $status;
2122
	}
2123
2124
	/**
2125
	 * @param User $user
2126
	 * @param string|false $oldModel false if the page is being newly created
2127
	 * @param string $newModel
2128
	 * @param string $reason
2129
	 */
2130
	protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2131
		$new = $oldModel === false;
2132
		$log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2133
		$log->setPerformer( $user );
2134
		$log->setTarget( $this->mTitle );
2135
		$log->setComment( $reason );
2136
		$log->setParameters( [
2137
			'4::oldmodel' => $oldModel,
2138
			'5::newmodel' => $newModel
2139
		] );
2140
		$logid = $log->insert();
2141
		$log->publish( $logid );
2142
	}
2143
2144
	/**
2145
	 * Register the change of watch status
2146
	 */
2147
	protected function updateWatchlist() {
2148
		global $wgUser;
2149
2150
		if ( !$wgUser->isLoggedIn() ) {
2151
			return;
2152
		}
2153
2154
		$user = $wgUser;
2155
		$title = $this->mTitle;
2156
		$watch = $this->watchthis;
2157
		// Do this in its own transaction to reduce contention...
2158
		DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2159
			if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2160
				return; // nothing to change
2161
			}
2162
			WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2163
		} );
2164
	}
2165
2166
	/**
2167
	 * Attempts to do 3-way merge of edit content with a base revision
2168
	 * and current content, in case of edit conflict, in whichever way appropriate
2169
	 * for the content type.
2170
	 *
2171
	 * @since 1.21
2172
	 *
2173
	 * @param Content $editContent
2174
	 *
2175
	 * @return bool
2176
	 */
2177
	private function mergeChangesIntoContent( &$editContent ) {
2178
2179
		$db = wfGetDB( DB_MASTER );
2180
2181
		// This is the revision the editor started from
2182
		$baseRevision = $this->getBaseRevision();
2183
		$baseContent = $baseRevision ? $baseRevision->getContent() : null;
2184
2185
		if ( is_null( $baseContent ) ) {
2186
			return false;
2187
		}
2188
2189
		// The current state, we want to merge updates into it
2190
		$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2191
		$currentContent = $currentRevision ? $currentRevision->getContent() : null;
2192
2193
		if ( is_null( $currentContent ) ) {
2194
			return false;
2195
		}
2196
2197
		$handler = ContentHandler::getForModelID( $baseContent->getModel() );
2198
2199
		$result = $handler->merge3( $baseContent, $editContent, $currentContent );
2200
2201
		if ( $result ) {
2202
			$editContent = $result;
2203
			// Update parentRevId to what we just merged.
2204
			$this->parentRevId = $currentRevision->getId();
2205
			return true;
2206
		}
2207
2208
		return false;
2209
	}
2210
2211
	/**
2212
	 * @note: this method is very poorly named. If the user opened the form with ?oldid=X,
2213
	 *        one might think of X as the "base revision", which is NOT what this returns.
2214
	 * @return Revision Current version when the edit was started
2215
	 */
2216
	function getBaseRevision() {
2217
		if ( !$this->mBaseRevision ) {
2218
			$db = wfGetDB( DB_MASTER );
2219
			$this->mBaseRevision = $this->editRevId
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->editRevId ? \Revi...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...
2220
				? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2221
				: Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2222
		}
2223
		return $this->mBaseRevision;
2224
	}
2225
2226
	/**
2227
	 * Check given input text against $wgSpamRegex, and return the text of the first match.
2228
	 *
2229
	 * @param string $text
2230
	 *
2231
	 * @return string|bool Matching string or false
2232
	 */
2233
	public static function matchSpamRegex( $text ) {
2234
		global $wgSpamRegex;
2235
		// For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2236
		$regexes = (array)$wgSpamRegex;
2237
		return self::matchSpamRegexInternal( $text, $regexes );
2238
	}
2239
2240
	/**
2241
	 * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
2242
	 *
2243
	 * @param string $text
2244
	 *
2245
	 * @return string|bool Matching string or false
2246
	 */
2247
	public static function matchSummarySpamRegex( $text ) {
2248
		global $wgSummarySpamRegex;
2249
		$regexes = (array)$wgSummarySpamRegex;
2250
		return self::matchSpamRegexInternal( $text, $regexes );
2251
	}
2252
2253
	/**
2254
	 * @param string $text
2255
	 * @param array $regexes
2256
	 * @return bool|string
2257
	 */
2258
	protected static function matchSpamRegexInternal( $text, $regexes ) {
2259
		foreach ( $regexes as $regex ) {
2260
			$matches = [];
2261
			if ( preg_match( $regex, $text, $matches ) ) {
2262
				return $matches[0];
2263
			}
2264
		}
2265
		return false;
2266
	}
2267
2268
	function setHeaders() {
2269
		global $wgOut, $wgUser, $wgAjaxEditStash;
2270
2271
		$wgOut->addModules( 'mediawiki.action.edit' );
2272
		$wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2273
2274
		if ( $wgUser->getOption( 'showtoolbar' ) ) {
2275
			// The addition of default buttons is handled by getEditToolbar() which
2276
			// has its own dependency on this module. The call here ensures the module
2277
			// is loaded in time (it has position "top") for other modules to register
2278
			// buttons (e.g. extensions, gadgets, user scripts).
2279
			$wgOut->addModules( 'mediawiki.toolbar' );
2280
		}
2281
2282
		if ( $wgUser->getOption( 'uselivepreview' ) ) {
2283
			$wgOut->addModules( 'mediawiki.action.edit.preview' );
2284
		}
2285
2286
		if ( $wgUser->getOption( 'useeditwarning' ) ) {
2287
			$wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2288
		}
2289
2290
		# Enabled article-related sidebar, toplinks, etc.
2291
		$wgOut->setArticleRelated( true );
2292
2293
		$contextTitle = $this->getContextTitle();
2294
		if ( $this->isConflict ) {
2295
			$msg = 'editconflict';
2296
		} elseif ( $contextTitle->exists() && $this->section != '' ) {
2297
			$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2298
		} else {
2299
			$msg = $contextTitle->exists()
2300
				|| ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2301
					&& $contextTitle->getDefaultMessageText() !== false
2302
				)
2303
				? 'editing'
2304
				: 'creating';
2305
		}
2306
2307
		# Use the title defined by DISPLAYTITLE magic word when present
2308
		# NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2309
		#       setPageTitle() treats the input as wikitext, which should be safe in either case.
2310
		$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2311
		if ( $displayTitle === false ) {
2312
			$displayTitle = $contextTitle->getPrefixedText();
2313
		}
2314
		$wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2315
		# Transmit the name of the message to JavaScript for live preview
2316
		# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2317
		$wgOut->addJsConfigVars( [
2318
			'wgEditMessage' => $msg,
2319
			'wgAjaxEditStash' => $wgAjaxEditStash,
2320
		] );
2321
	}
2322
2323
	/**
2324
	 * Show all applicable editing introductions
2325
	 */
2326
	protected function showIntro() {
2327
		global $wgOut, $wgUser;
2328
		if ( $this->suppressIntro ) {
2329
			return;
2330
		}
2331
2332
		$namespace = $this->mTitle->getNamespace();
2333
2334
		if ( $namespace == NS_MEDIAWIKI ) {
2335
			# Show a warning if editing an interface message
2336
			$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2337
			# If this is a default message (but not css or js),
2338
			# show a hint that it is translatable on translatewiki.net
2339
			if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2340
				&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2341
			) {
2342
				$defaultMessageText = $this->mTitle->getDefaultMessageText();
2343
				if ( $defaultMessageText !== false ) {
2344
					$wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2345
						'translateinterface' );
2346
				}
2347
			}
2348
		} elseif ( $namespace == NS_FILE ) {
2349
			# Show a hint to shared repo
2350
			$file = wfFindFile( $this->mTitle );
2351
			if ( $file && !$file->isLocal() ) {
2352
				$descUrl = $file->getDescriptionUrl();
2353
				# there must be a description url to show a hint to shared repo
2354
				if ( $descUrl ) {
2355
					if ( !$this->mTitle->exists() ) {
2356
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2357
									'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2358
						] );
2359
					} else {
2360
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2361
									'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2362
						] );
2363
					}
2364
				}
2365
			}
2366
		}
2367
2368
		# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2369
		# Show log extract when the user is currently blocked
2370
		if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2371
			$username = explode( '/', $this->mTitle->getText(), 2 )[0];
2372
			$user = User::newFromName( $username, false /* allow IP users*/ );
2373
			$ip = User::isIP( $username );
2374
			$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 2372 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 2372 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...
2375
			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2376
				$wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2377
					[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2378 View Code Duplication
			} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2379
				# Show log extract if the user is currently blocked
2380
				LogEventsList::showLogExtract(
2381
					$wgOut,
2382
					'block',
2383
					MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2384
					'',
2385
					[
2386
						'lim' => 1,
2387
						'showIfEmpty' => false,
2388
						'msgKey' => [
2389
							'blocked-notice-logextract',
2390
							$user->getName() # Support GENDER in notice
2391
						]
2392
					]
2393
				);
2394
			}
2395
		}
2396
		# Try to add a custom edit intro, or use the standard one if this is not possible.
2397
		if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2398
			$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2399
				wfMessage( 'helppage' )->inContentLanguage()->text()
2400
			) );
2401
			if ( $wgUser->isLoggedIn() ) {
2402
				$wgOut->wrapWikiMsg(
2403
					// Suppress the external link icon, consider the help url an internal one
2404
					"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2405
					[
2406
						'newarticletext',
2407
						$helpLink
2408
					]
2409
				);
2410
			} else {
2411
				$wgOut->wrapWikiMsg(
2412
					// Suppress the external link icon, consider the help url an internal one
2413
					"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2414
					[
2415
						'newarticletextanon',
2416
						$helpLink
2417
					]
2418
				);
2419
			}
2420
		}
2421
		# Give a notice if the user is editing a deleted/moved page...
2422 View Code Duplication
		if ( !$this->mTitle->exists() ) {
2423
			LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2424
				'',
2425
				[
2426
					'lim' => 10,
2427
					'conds' => [ "log_action != 'revision'" ],
2428
					'showIfEmpty' => false,
2429
					'msgKey' => [ 'recreate-moveddeleted-warn' ]
2430
				]
2431
			);
2432
		}
2433
	}
2434
2435
	/**
2436
	 * Attempt to show a custom editing introduction, if supplied
2437
	 *
2438
	 * @return bool
2439
	 */
2440
	protected function showCustomIntro() {
2441
		if ( $this->editintro ) {
2442
			$title = Title::newFromText( $this->editintro );
2443
			if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2444
				global $wgOut;
2445
				// Added using template syntax, to take <noinclude>'s into account.
2446
				$wgOut->addWikiTextTitleTidy(
2447
					'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2448
					$this->mTitle
2449
				);
2450
				return true;
2451
			}
2452
		}
2453
		return false;
2454
	}
2455
2456
	/**
2457
	 * Gets an editable textual representation of $content.
2458
	 * The textual representation can be turned by into a Content object by the
2459
	 * toEditContent() method.
2460
	 *
2461
	 * If $content is null or false or a string, $content is returned unchanged.
2462
	 *
2463
	 * If the given Content object is not of a type that can be edited using
2464
	 * the text base EditPage, an exception will be raised. Set
2465
	 * $this->allowNonTextContent to true to allow editing of non-textual
2466
	 * content.
2467
	 *
2468
	 * @param Content|null|bool|string $content
2469
	 * @return string The editable text form of the content.
2470
	 *
2471
	 * @throws MWException If $content is not an instance of TextContent and
2472
	 *   $this->allowNonTextContent is not true.
2473
	 */
2474
	protected function toEditText( $content ) {
2475
		if ( $content === null || $content === false || is_string( $content ) ) {
2476
			return $content;
2477
		}
2478
2479
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2480
			throw new MWException( 'This content model is not supported: '
2481
				. 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...
2482
		}
2483
2484
		return $content->serialize( $this->contentFormat );
2485
	}
2486
2487
	/**
2488
	 * Turns the given text into a Content object by unserializing it.
2489
	 *
2490
	 * If the resulting Content object is not of a type that can be edited using
2491
	 * the text base EditPage, an exception will be raised. Set
2492
	 * $this->allowNonTextContent to true to allow editing of non-textual
2493
	 * content.
2494
	 *
2495
	 * @param string|null|bool $text Text to unserialize
2496
	 * @return Content The content object created from $text. If $text was false
2497
	 *   or null, false resp. null will be  returned instead.
2498
	 *
2499
	 * @throws MWException If unserializing the text results in a Content
2500
	 *   object that is not an instance of TextContent and
2501
	 *   $this->allowNonTextContent is not true.
2502
	 */
2503
	protected function toEditContent( $text ) {
2504
		if ( $text === false || $text === null ) {
2505
			return $text;
2506
		}
2507
2508
		$content = ContentHandler::makeContent( $text, $this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $text defined by parameter $text on line 2503 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...
2509
			$this->contentModel, $this->contentFormat );
2510
2511
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2512
			throw new MWException( 'This content model is not supported: '
2513
				. ContentHandler::getLocalizedName( $content->getModel() ) );
2514
		}
2515
2516
		return $content;
2517
	}
2518
2519
	/**
2520
	 * Send the edit form and related headers to $wgOut
2521
	 * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2522
	 *     during form output near the top, for captchas and the like.
2523
	 *
2524
	 * The $formCallback parameter is deprecated since MediaWiki 1.25. Please
2525
	 * use the EditPage::showEditForm:fields hook instead.
2526
	 */
2527
	function showEditForm( $formCallback = null ) {
2528
		global $wgOut, $wgUser;
2529
2530
		# need to parse the preview early so that we know which templates are used,
2531
		# otherwise users with "show preview after edit box" will get a blank list
2532
		# we parse this near the beginning so that setHeaders can do the title
2533
		# setting work instead of leaving it in getPreviewText
2534
		$previewOutput = '';
2535
		if ( $this->formtype == 'preview' ) {
2536
			$previewOutput = $this->getPreviewText();
2537
		}
2538
2539
		Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2540
2541
		$this->setHeaders();
2542
2543
		if ( $this->showHeader() === false ) {
2544
			return;
2545
		}
2546
2547
		$wgOut->addHTML( $this->editFormPageTop );
2548
2549
		if ( $wgUser->getOption( 'previewontop' ) ) {
2550
			$this->displayPreviewArea( $previewOutput, true );
2551
		}
2552
2553
		$wgOut->addHTML( $this->editFormTextTop );
2554
2555
		$showToolbar = true;
2556
		if ( $this->wasDeletedSinceLastEdit() ) {
2557
			if ( $this->formtype == 'save' ) {
2558
				// Hide the toolbar and edit area, user can click preview to get it back
2559
				// Add an confirmation checkbox and explanation.
2560
				$showToolbar = false;
2561
			} else {
2562
				$wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2563
					'deletedwhileediting' );
2564
			}
2565
		}
2566
2567
		// @todo add EditForm plugin interface and use it here!
2568
		//       search for textarea1 and textares2, and allow EditForm to override all uses.
2569
		$wgOut->addHTML( Html::openElement(
2570
			'form',
2571
			[
2572
				'id' => self::EDITFORM_ID,
2573
				'name' => self::EDITFORM_ID,
2574
				'method' => 'post',
2575
				'action' => $this->getActionURL( $this->getContextTitle() ),
2576
				'enctype' => 'multipart/form-data'
2577
			]
2578
		) );
2579
2580
		if ( is_callable( $formCallback ) ) {
2581
			wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2582
			call_user_func_array( $formCallback, [ &$wgOut ] );
2583
		}
2584
2585
		// Add an empty field to trip up spambots
2586
		$wgOut->addHTML(
2587
			Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2588
			. Html::rawElement(
2589
				'label',
2590
				[ 'for' => 'wpAntispam' ],
2591
				wfMessage( 'simpleantispam-label' )->parse()
2592
			)
2593
			. Xml::element(
2594
				'input',
2595
				[
2596
					'type' => 'text',
2597
					'name' => 'wpAntispam',
2598
					'id' => 'wpAntispam',
2599
					'value' => ''
2600
				]
2601
			)
2602
			. Xml::closeElement( 'div' )
2603
		);
2604
2605
		Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2606
2607
		// Put these up at the top to ensure they aren't lost on early form submission
2608
		$this->showFormBeforeText();
2609
2610
		if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2611
			$username = $this->lastDelete->user_name;
2612
			$comment = $this->lastDelete->log_comment;
2613
2614
			// It is better to not parse the comment at all than to have templates expanded in the middle
2615
			// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2616
			$key = $comment === ''
2617
				? 'confirmrecreate-noreason'
2618
				: 'confirmrecreate';
2619
			$wgOut->addHTML(
2620
				'<div class="mw-confirm-recreate">' .
2621
					wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2622
				Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2623
					[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2624
				) .
2625
				'</div>'
2626
			);
2627
		}
2628
2629
		# When the summary is hidden, also hide them on preview/show changes
2630
		if ( $this->nosummary ) {
2631
			$wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2632
		}
2633
2634
		# If a blank edit summary was previously provided, and the appropriate
2635
		# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2636
		# user being bounced back more than once in the event that a summary
2637
		# is not required.
2638
		# ####
2639
		# For a bit more sophisticated detection of blank summaries, hash the
2640
		# automatic one and pass that in the hidden field wpAutoSummary.
2641
		if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2642
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2643
		}
2644
2645
		if ( $this->undidRev ) {
2646
			$wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2647
		}
2648
2649
		if ( $this->selfRedirect ) {
2650
			$wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2651
		}
2652
2653
		if ( $this->hasPresetSummary ) {
2654
			// If a summary has been preset using &summary= we don't want to prompt for
2655
			// a different summary. Only prompt for a summary if the summary is blanked.
2656
			// (Bug 17416)
2657
			$this->autoSumm = md5( '' );
2658
		}
2659
2660
		$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2661
		$wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2662
2663
		$wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2664
		$wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2665
2666
		$wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2667
		$wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2668
2669 View Code Duplication
		if ( $this->section == 'new' ) {
2670
			$this->showSummaryInput( true, $this->summary );
2671
			$wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2672
		}
2673
2674
		$wgOut->addHTML( $this->editFormTextBeforeContent );
2675
2676
		if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2677
			$wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2678
		}
2679
2680
		if ( $this->blankArticle ) {
2681
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2682
		}
2683
2684
		if ( $this->isConflict ) {
2685
			// In an edit conflict bypass the overridable content form method
2686
			// and fallback to the raw wpTextbox1 since editconflicts can't be
2687
			// resolved between page source edits and custom ui edits using the
2688
			// custom edit ui.
2689
			$this->textbox2 = $this->textbox1;
2690
2691
			$content = $this->getCurrentContent();
2692
			$this->textbox1 = $this->toEditText( $content );
2693
2694
			$this->showTextbox1();
2695
		} else {
2696
			$this->showContentForm();
2697
		}
2698
2699
		$wgOut->addHTML( $this->editFormTextAfterContent );
2700
2701
		$this->showStandardInputs();
2702
2703
		$this->showFormAfterText();
2704
2705
		$this->showTosSummary();
2706
2707
		$this->showEditTools();
2708
2709
		$wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2710
2711
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2712
			Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2713
2714
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2715
			Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2716
2717
		if ( $this->mParserOutput ) {
2718
			$wgOut->setLimitReportData( $this->mParserOutput->getLimitReportData() );
2719
		}
2720
2721
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2722
2723 View Code Duplication
		if ( $this->isConflict ) {
2724
			try {
2725
				$this->showConflict();
2726
			} catch ( MWContentSerializationException $ex ) {
2727
				// this can't really happen, but be nice if it does.
2728
				$msg = wfMessage(
2729
					'content-failed-to-parse',
2730
					$this->contentModel,
2731
					$this->contentFormat,
2732
					$ex->getMessage()
2733
				);
2734
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2735
			}
2736
		}
2737
2738
		// Set a hidden field so JS knows what edit form mode we are in
2739
		if ( $this->isConflict ) {
2740
			$mode = 'conflict';
2741
		} elseif ( $this->preview ) {
2742
			$mode = 'preview';
2743
		} elseif ( $this->diff ) {
2744
			$mode = 'diff';
2745
		} else {
2746
			$mode = 'text';
2747
		}
2748
		$wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2749
2750
		// Marker for detecting truncated form data.  This must be the last
2751
		// parameter sent in order to be of use, so do not move me.
2752
		$wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2753
		$wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2754
2755
		if ( !$wgUser->getOption( 'previewontop' ) ) {
2756
			$this->displayPreviewArea( $previewOutput, false );
2757
		}
2758
2759
	}
2760
2761
	/**
2762
	 * Extract the section title from current section text, if any.
2763
	 *
2764
	 * @param string $text
2765
	 * @return string|bool String or false
2766
	 */
2767
	public static function extractSectionTitle( $text ) {
2768
		preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2769
		if ( !empty( $matches[2] ) ) {
2770
			global $wgParser;
2771
			return $wgParser->stripSectionName( trim( $matches[2] ) );
2772
		} else {
2773
			return false;
2774
		}
2775
	}
2776
2777
	/**
2778
	 * @return bool
2779
	 */
2780
	protected function showHeader() {
2781
		global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
2782
		global $wgAllowUserCss, $wgAllowUserJs;
2783
2784
		if ( $this->mTitle->isTalkPage() ) {
2785
			$wgOut->addWikiMsg( 'talkpagetext' );
2786
		}
2787
2788
		// Add edit notices
2789
		$editNotices = $this->mTitle->getEditNotices( $this->oldid );
2790
		if ( count( $editNotices ) ) {
2791
			$wgOut->addHTML( implode( "\n", $editNotices ) );
2792
		} else {
2793
			$msg = wfMessage( 'editnotice-notext' );
2794
			if ( !$msg->isDisabled() ) {
2795
				$wgOut->addHTML(
2796
					'<div class="mw-editnotice-notext">'
2797
					. $msg->parseAsBlock()
2798
					. '</div>'
2799
				);
2800
			}
2801
		}
2802
2803
		if ( $this->isConflict ) {
2804
			$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2805
			$this->editRevId = $this->page->getLatest();
2806
		} else {
2807
			if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2808
				// We use $this->section to much before this and getVal('wgSection') directly in other places
2809
				// at this point we can't reset $this->section to '' to fallback to non-section editing.
2810
				// Someone is welcome to try refactoring though
2811
				$wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2812
				return false;
2813
			}
2814
2815
			if ( $this->section != '' && $this->section != 'new' ) {
2816
				if ( !$this->summary && !$this->preview && !$this->diff ) {
2817
					$sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2818
					if ( $sectionTitle !== false ) {
2819
						$this->summary = "/* $sectionTitle */ ";
2820
					}
2821
				}
2822
			}
2823
2824
			if ( $this->missingComment ) {
2825
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2826
			}
2827
2828
			if ( $this->missingSummary && $this->section != 'new' ) {
2829
				$wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2830
			}
2831
2832
			if ( $this->missingSummary && $this->section == 'new' ) {
2833
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2834
			}
2835
2836
			if ( $this->blankArticle ) {
2837
				$wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2838
			}
2839
2840
			if ( $this->selfRedirect ) {
2841
				$wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2842
			}
2843
2844
			if ( $this->hookError !== '' ) {
2845
				$wgOut->addWikiText( $this->hookError );
2846
			}
2847
2848
			if ( !$this->checkUnicodeCompliantBrowser() ) {
2849
				$wgOut->addWikiMsg( 'nonunicodebrowser' );
2850
			}
2851
2852
			if ( $this->section != 'new' ) {
2853
				$revision = $this->mArticle->getRevisionFetched();
2854
				if ( $revision ) {
2855
					// Let sysop know that this will make private content public if saved
2856
2857 View Code Duplication
					if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2858
						$wgOut->wrapWikiMsg(
2859
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2860
							'rev-deleted-text-permission'
2861
						);
2862
					} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2863
						$wgOut->wrapWikiMsg(
2864
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2865
							'rev-deleted-text-view'
2866
						);
2867
					}
2868
2869
					if ( !$revision->isCurrent() ) {
2870
						$this->mArticle->setOldSubtitle( $revision->getId() );
2871
						$wgOut->addWikiMsg( 'editingold' );
2872
					}
2873
				} elseif ( $this->mTitle->exists() ) {
2874
					// Something went wrong
2875
2876
					$wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2877
						[ 'missing-revision', $this->oldid ] );
2878
				}
2879
			}
2880
		}
2881
2882
		if ( wfReadOnly() ) {
2883
			$wgOut->wrapWikiMsg(
2884
				"<div id=\"mw-read-only-warning\">\n$1\n</div>",
2885
				[ 'readonlywarning', wfReadOnlyReason() ]
2886
			);
2887
		} elseif ( $wgUser->isAnon() ) {
2888
			if ( $this->formtype != 'preview' ) {
2889
				$wgOut->wrapWikiMsg(
2890
					"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2891
					[ 'anoneditwarning',
2892
						// Log-in link
2893
						SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2894
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2895
						] ),
2896
						// Sign-up link
2897
						SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2898
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2899
						] )
2900
					]
2901
				);
2902
			} else {
2903
				$wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2904
					'anonpreviewwarning'
2905
				);
2906
			}
2907
		} else {
2908
			if ( $this->isCssJsSubpage ) {
2909
				# Check the skin exists
2910
				if ( $this->isWrongCaseCssJsPage ) {
2911
					$wgOut->wrapWikiMsg(
2912
						"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2913
						[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2914
					);
2915
				}
2916
				if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2917
					if ( $this->formtype !== 'preview' ) {
2918
						if ( $this->isCssSubpage && $wgAllowUserCss ) {
2919
							$wgOut->wrapWikiMsg(
2920
								"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2921
								[ 'usercssyoucanpreview' ]
2922
							);
2923
						}
2924
2925
						if ( $this->isJsSubpage && $wgAllowUserJs ) {
2926
							$wgOut->wrapWikiMsg(
2927
								"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2928
								[ 'userjsyoucanpreview' ]
2929
							);
2930
						}
2931
					}
2932
				}
2933
			}
2934
		}
2935
2936
		if ( $this->mTitle->isProtected( 'edit' ) &&
2937
			MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2938
		) {
2939
			# Is the title semi-protected?
2940
			if ( $this->mTitle->isSemiProtected() ) {
2941
				$noticeMsg = 'semiprotectedpagewarning';
2942
			} else {
2943
				# Then it must be protected based on static groups (regular)
2944
				$noticeMsg = 'protectedpagewarning';
2945
			}
2946
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2947
				[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2948
		}
2949
		if ( $this->mTitle->isCascadeProtected() ) {
2950
			# Is this page under cascading protection from some source pages?
2951
			/** @var Title[] $cascadeSources */
2952
			list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2953
			$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2954
			$cascadeSourcesCount = count( $cascadeSources );
2955
			if ( $cascadeSourcesCount > 0 ) {
2956
				# Explain, and list the titles responsible
2957
				foreach ( $cascadeSources as $page ) {
2958
					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2959
				}
2960
			}
2961
			$notice .= '</div>';
2962
			$wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2963
		}
2964
		if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2965
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2966
				[ 'lim' => 1,
2967
					'showIfEmpty' => false,
2968
					'msgKey' => [ 'titleprotectedwarning' ],
2969
					'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2970
		}
2971
2972
		if ( $this->contentLength === false ) {
2973
			$this->contentLength = strlen( $this->textbox1 );
2974
		}
2975
2976
		if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
2977
			$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2978
				[
2979
					'longpageerror',
2980
					$wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
2981
					$wgLang->formatNum( $wgMaxArticleSize )
2982
				]
2983
			);
2984
		} else {
2985
			if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2986
				$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2987
					[
2988
						'longpage-hint',
2989
						$wgLang->formatSize( strlen( $this->textbox1 ) ),
2990
						strlen( $this->textbox1 )
2991
					]
2992
				);
2993
			}
2994
		}
2995
		# Add header copyright warning
2996
		$this->showHeaderCopyrightWarning();
2997
2998
		return true;
2999
	}
3000
3001
	/**
3002
	 * Standard summary input and label (wgSummary), abstracted so EditPage
3003
	 * subclasses may reorganize the form.
3004
	 * Note that you do not need to worry about the label's for=, it will be
3005
	 * inferred by the id given to the input. You can remove them both by
3006
	 * passing array( 'id' => false ) to $userInputAttrs.
3007
	 *
3008
	 * @param string $summary The value of the summary input
3009
	 * @param string $labelText The html to place inside the label
3010
	 * @param array $inputAttrs Array of attrs to use on the input
3011
	 * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
3012
	 *
3013
	 * @return array An array in the format array( $label, $input )
3014
	 */
3015
	function getSummaryInput( $summary = "", $labelText = null,
3016
		$inputAttrs = null, $spanLabelAttrs = null
3017
	) {
3018
		// Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
3019
		$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3020
			'id' => 'wpSummary',
3021
			'maxlength' => '200',
3022
			'tabindex' => '1',
3023
			'size' => 60,
3024
			'spellcheck' => 'true',
3025
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
3026
3027
		$spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3028
			'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3029
			'id' => "wpSummaryLabel"
3030
		];
3031
3032
		$label = null;
3033
		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...
3034
			$label = Xml::tags(
3035
				'label',
3036
				$inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3037
				$labelText
3038
			);
3039
			$label = Xml::tags( 'span', $spanLabelAttrs, $label );
3040
		}
3041
3042
		$input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3043
3044
		return [ $label, $input ];
3045
	}
3046
3047
	/**
3048
	 * @param bool $isSubjectPreview True if this is the section subject/title
3049
	 *   up top, or false if this is the comment summary
3050
	 *   down below the textarea
3051
	 * @param string $summary The text of the summary to display
3052
	 */
3053
	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3054
		global $wgOut;
3055
		# Add a class if 'missingsummary' is triggered to allow styling of the summary line
3056
		$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3057
		if ( $isSubjectPreview ) {
3058
			if ( $this->nosummary ) {
3059
				return;
3060
			}
3061
		} else {
3062
			if ( !$this->mShowSummaryField ) {
3063
				return;
3064
			}
3065
		}
3066
		$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3067
		list( $label, $input ) = $this->getSummaryInput(
3068
			$summary,
3069
			$labelText,
3070
			[ 'class' => $summaryClass ],
3071
			[]
3072
		);
3073
		$wgOut->addHTML( "{$label} {$input}" );
3074
	}
3075
3076
	/**
3077
	 * @param bool $isSubjectPreview True if this is the section subject/title
3078
	 *   up top, or false if this is the comment summary
3079
	 *   down below the textarea
3080
	 * @param string $summary The text of the summary to display
3081
	 * @return string
3082
	 */
3083
	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3084
		// avoid spaces in preview, gets always trimmed on save
3085
		$summary = trim( $summary );
3086
		if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3087
			return "";
3088
		}
3089
3090
		global $wgParser;
3091
3092
		if ( $isSubjectPreview ) {
3093
			$summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3094
				->inContentLanguage()->text();
3095
		}
3096
3097
		$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3098
3099
		$summary = wfMessage( $message )->parse()
3100
			. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3101
		return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3102
	}
3103
3104
	protected function showFormBeforeText() {
3105
		global $wgOut;
3106
		$section = htmlspecialchars( $this->section );
3107
		$wgOut->addHTML( <<<HTML
3108
<input type='hidden' value="{$section}" name="wpSection"/>
3109
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3110
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3111
<input type='hidden' value="{$this->editRevId}" name="editRevId" />
3112
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3113
3114
HTML
3115
		);
3116
		if ( !$this->checkUnicodeCompliantBrowser() ) {
3117
			$wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3118
		}
3119
	}
3120
3121
	protected function showFormAfterText() {
3122
		global $wgOut, $wgUser;
3123
		/**
3124
		 * To make it harder for someone to slip a user a page
3125
		 * which submits an edit form to the wiki without their
3126
		 * knowledge, a random token is associated with the login
3127
		 * session. If it's not passed back with the submission,
3128
		 * we won't save the page, or render user JavaScript and
3129
		 * CSS previews.
3130
		 *
3131
		 * For anon editors, who may not have a session, we just
3132
		 * include the constant suffix to prevent editing from
3133
		 * broken text-mangling proxies.
3134
		 */
3135
		$wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3136
	}
3137
3138
	/**
3139
	 * Subpage overridable method for printing the form for page content editing
3140
	 * By default this simply outputs wpTextbox1
3141
	 * Subclasses can override this to provide a custom UI for editing;
3142
	 * be it a form, or simply wpTextbox1 with a modified content that will be
3143
	 * reverse modified when extracted from the post data.
3144
	 * Note that this is basically the inverse for importContentFormData
3145
	 */
3146
	protected function showContentForm() {
3147
		$this->showTextbox1();
3148
	}
3149
3150
	/**
3151
	 * Method to output wpTextbox1
3152
	 * The $textoverride method can be used by subclasses overriding showContentForm
3153
	 * to pass back to this method.
3154
	 *
3155
	 * @param array $customAttribs Array of html attributes to use in the textarea
3156
	 * @param string $textoverride Optional text to override $this->textarea1 with
3157
	 */
3158
	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3159
		if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3160
			$attribs = [ 'style' => 'display:none;' ];
3161
		} else {
3162
			$classes = []; // Textarea CSS
3163
			if ( $this->mTitle->isProtected( 'edit' ) &&
3164
				MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3165
			) {
3166
				# Is the title semi-protected?
3167
				if ( $this->mTitle->isSemiProtected() ) {
3168
					$classes[] = 'mw-textarea-sprotected';
3169
				} else {
3170
					# Then it must be protected based on static groups (regular)
3171
					$classes[] = 'mw-textarea-protected';
3172
				}
3173
				# Is the title cascade-protected?
3174
				if ( $this->mTitle->isCascadeProtected() ) {
3175
					$classes[] = 'mw-textarea-cprotected';
3176
				}
3177
			}
3178
3179
			$attribs = [ 'tabindex' => 1 ];
3180
3181
			if ( is_array( $customAttribs ) ) {
3182
				$attribs += $customAttribs;
3183
			}
3184
3185
			if ( count( $classes ) ) {
3186
				if ( isset( $attribs['class'] ) ) {
3187
					$classes[] = $attribs['class'];
3188
				}
3189
				$attribs['class'] = implode( ' ', $classes );
3190
			}
3191
		}
3192
3193
		$this->showTextbox(
3194
			$textoverride !== null ? $textoverride : $this->textbox1,
3195
			'wpTextbox1',
3196
			$attribs
3197
		);
3198
	}
3199
3200
	protected function showTextbox2() {
3201
		$this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3202
	}
3203
3204
	protected function showTextbox( $text, $name, $customAttribs = [] ) {
3205
		global $wgOut, $wgUser;
3206
3207
		$wikitext = $this->safeUnicodeOutput( $text );
3208
		if ( strval( $wikitext ) !== '' ) {
3209
			// Ensure there's a newline at the end, otherwise adding lines
3210
			// is awkward.
3211
			// But don't add a newline if the ext is empty, or Firefox in XHTML
3212
			// mode will show an extra newline. A bit annoying.
3213
			$wikitext .= "\n";
3214
		}
3215
3216
		$attribs = $customAttribs + [
3217
			'accesskey' => ',',
3218
			'id' => $name,
3219
			'cols' => $wgUser->getIntOption( 'cols' ),
3220
			'rows' => $wgUser->getIntOption( 'rows' ),
3221
			// Avoid PHP notices when appending preferences
3222
			// (appending allows customAttribs['style'] to still work).
3223
			'style' => ''
3224
		];
3225
3226
		$pageLang = $this->mTitle->getPageLanguage();
3227
		$attribs['lang'] = $pageLang->getHtmlCode();
3228
		$attribs['dir'] = $pageLang->getDir();
3229
3230
		$wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3231
	}
3232
3233
	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3234
		global $wgOut;
3235
		$classes = [];
3236
		if ( $isOnTop ) {
3237
			$classes[] = 'ontop';
3238
		}
3239
3240
		$attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3241
3242
		if ( $this->formtype != 'preview' ) {
3243
			$attribs['style'] = 'display: none;';
3244
		}
3245
3246
		$wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3247
3248
		if ( $this->formtype == 'preview' ) {
3249
			$this->showPreview( $previewOutput );
3250
		} else {
3251
			// Empty content container for LivePreview
3252
			$pageViewLang = $this->mTitle->getPageViewLanguage();
3253
			$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3254
				'class' => 'mw-content-' . $pageViewLang->getDir() ];
3255
			$wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3256
		}
3257
3258
		$wgOut->addHTML( '</div>' );
3259
3260 View Code Duplication
		if ( $this->formtype == 'diff' ) {
3261
			try {
3262
				$this->showDiff();
3263
			} catch ( MWContentSerializationException $ex ) {
3264
				$msg = wfMessage(
3265
					'content-failed-to-parse',
3266
					$this->contentModel,
3267
					$this->contentFormat,
3268
					$ex->getMessage()
3269
				);
3270
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3271
			}
3272
		}
3273
	}
3274
3275
	/**
3276
	 * Append preview output to $wgOut.
3277
	 * Includes category rendering if this is a category page.
3278
	 *
3279
	 * @param string $text The HTML to be output for the preview.
3280
	 */
3281
	protected function showPreview( $text ) {
3282
		global $wgOut;
3283
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3284
			$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...
3285
		}
3286
		# This hook seems slightly odd here, but makes things more
3287
		# consistent for extensions.
3288
		Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3289
		$wgOut->addHTML( $text );
3290
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3291
			$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...
3292
		}
3293
	}
3294
3295
	/**
3296
	 * Get a diff between the current contents of the edit box and the
3297
	 * version of the page we're editing from.
3298
	 *
3299
	 * If this is a section edit, we'll replace the section as for final
3300
	 * save and then make a comparison.
3301
	 */
3302
	function showDiff() {
3303
		global $wgUser, $wgContLang, $wgOut;
3304
3305
		$oldtitlemsg = 'currentrev';
3306
		# if message does not exist, show diff against the preloaded default
3307
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3308
			$oldtext = $this->mTitle->getDefaultMessageText();
3309
			if ( $oldtext !== false ) {
3310
				$oldtitlemsg = 'defaultmessagetext';
3311
				$oldContent = $this->toEditContent( $oldtext );
3312
			} else {
3313
				$oldContent = null;
3314
			}
3315
		} else {
3316
			$oldContent = $this->getCurrentContent();
3317
		}
3318
3319
		$textboxContent = $this->toEditContent( $this->textbox1 );
3320
		if ( $this->editRevId !== null ) {
3321
			$newContent = $this->page->replaceSectionAtRev(
3322
				$this->section, $textboxContent, $this->summary, $this->editRevId
0 ignored issues
show
Bug introduced by
It seems like $textboxContent defined by $this->toEditContent($this->textbox1) on line 3319 can be null; however, WikiPage::replaceSectionAtRev() 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...
3323
			);
3324
		} else {
3325
			$newContent = $this->page->replaceSectionContent(
3326
				$this->section, $textboxContent, $this->summary, $this->edittime
0 ignored issues
show
Bug introduced by
It seems like $textboxContent defined by $this->toEditContent($this->textbox1) on line 3319 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...
3327
			);
3328
		}
3329
3330
		if ( $newContent ) {
3331
			ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3332
			Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3333
3334
			$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3335
			$newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3336
		}
3337
3338
		if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3339
			$oldtitle = wfMessage( $oldtitlemsg )->parse();
3340
			$newtitle = wfMessage( 'yourtext' )->parse();
3341
3342
			if ( !$oldContent ) {
3343
				$oldContent = $newContent->getContentHandler()->makeEmptyContent();
3344
			}
3345
3346
			if ( !$newContent ) {
3347
				$newContent = $oldContent->getContentHandler()->makeEmptyContent();
3348
			}
3349
3350
			$de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3351
			$de->setContent( $oldContent, $newContent );
3352
3353
			$difftext = $de->getDiff( $oldtitle, $newtitle );
3354
			$de->showDiffStyle();
3355
		} else {
3356
			$difftext = '';
3357
		}
3358
3359
		$wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3360
	}
3361
3362
	/**
3363
	 * Show the header copyright warning.
3364
	 */
3365
	protected function showHeaderCopyrightWarning() {
3366
		$msg = 'editpage-head-copy-warn';
3367
		if ( !wfMessage( $msg )->isDisabled() ) {
3368
			global $wgOut;
3369
			$wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3370
				'editpage-head-copy-warn' );
3371
		}
3372
	}
3373
3374
	/**
3375
	 * Give a chance for site and per-namespace customizations of
3376
	 * terms of service summary link that might exist separately
3377
	 * from the copyright notice.
3378
	 *
3379
	 * This will display between the save button and the edit tools,
3380
	 * so should remain short!
3381
	 */
3382
	protected function showTosSummary() {
3383
		$msg = 'editpage-tos-summary';
3384
		Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3385
		if ( !wfMessage( $msg )->isDisabled() ) {
3386
			global $wgOut;
3387
			$wgOut->addHTML( '<div class="mw-tos-summary">' );
3388
			$wgOut->addWikiMsg( $msg );
3389
			$wgOut->addHTML( '</div>' );
3390
		}
3391
	}
3392
3393
	protected function showEditTools() {
3394
		global $wgOut;
3395
		$wgOut->addHTML( '<div class="mw-editTools">' .
3396
			wfMessage( 'edittools' )->inContentLanguage()->parse() .
3397
			'</div>' );
3398
	}
3399
3400
	/**
3401
	 * Get the copyright warning
3402
	 *
3403
	 * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3404
	 * @return string
3405
	 */
3406
	protected function getCopywarn() {
3407
		return self::getCopyrightWarning( $this->mTitle );
3408
	}
3409
3410
	/**
3411
	 * Get the copyright warning, by default returns wikitext
3412
	 *
3413
	 * @param Title $title
3414
	 * @param string $format Output format, valid values are any function of a Message object
3415
	 * @return string
3416
	 */
3417
	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3418
		global $wgRightsText;
3419
		if ( $wgRightsText ) {
3420
			$copywarnMsg = [ 'copyrightwarning',
3421
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3422
				$wgRightsText ];
3423
		} else {
3424
			$copywarnMsg = [ 'copyrightwarning2',
3425
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3426
		}
3427
		// Allow for site and per-namespace customization of contribution/copyright notice.
3428
		Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3429
3430
		return "<div id=\"editpage-copywarn\">\n" .
3431
			call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3432
	}
3433
3434
	/**
3435
	 * Get the Limit report for page previews
3436
	 *
3437
	 * @since 1.22
3438
	 * @param ParserOutput $output ParserOutput object from the parse
3439
	 * @return string HTML
3440
	 */
3441
	public static function getPreviewLimitReport( $output ) {
3442
		if ( !$output || !$output->getLimitReportData() ) {
3443
			return '';
3444
		}
3445
3446
		return ResourceLoader::makeInlineScript(
3447
			ResourceLoader::makeConfigSetScript(
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeCon...mitReportData()), true) targeting ResourceLoader::makeConfigSetScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
3448
				[ 'wgPageParseReport' => $output->getLimitReportData() ],
3449
				true
3450
			)
3451
		);
3452
	}
3453
3454
	protected function showStandardInputs( &$tabindex = 2 ) {
3455
		global $wgOut;
3456
		$wgOut->addHTML( "<div class='editOptions'>\n" );
3457
3458 View Code Duplication
		if ( $this->section != 'new' ) {
3459
			$this->showSummaryInput( false, $this->summary );
3460
			$wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3461
		}
3462
3463
		$checkboxes = $this->getCheckboxes( $tabindex,
3464
			[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3465
		$wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3466
3467
		// Show copyright warning.
3468
		$wgOut->addWikiText( $this->getCopywarn() );
3469
		$wgOut->addHTML( $this->editFormTextAfterWarn );
3470
3471
		$wgOut->addHTML( "<div class='editButtons'>\n" );
3472
		$wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3473
3474
		$cancel = $this->getCancelLink();
3475
		if ( $cancel !== '' ) {
3476
			$cancel .= Html::element( 'span',
3477
				[ 'class' => 'mw-editButtons-pipe-separator' ],
3478
				wfMessage( 'pipe-separator' )->text() );
3479
		}
3480
3481
		$message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3482
		$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3483
		$attrs = [
3484
			'target' => 'helpwindow',
3485
			'href' => $edithelpurl,
3486
		];
3487
		$edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3488
			$attrs, [ 'mw-ui-quiet' ] ) .
3489
			wfMessage( 'word-separator' )->escaped() .
3490
			wfMessage( 'newwindow' )->parse();
3491
3492
		$wgOut->addHTML( "	<span class='cancelLink'>{$cancel}</span>\n" );
3493
		$wgOut->addHTML( "	<span class='editHelp'>{$edithelp}</span>\n" );
3494
		$wgOut->addHTML( "</div><!-- editButtons -->\n" );
3495
3496
		Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3497
3498
		$wgOut->addHTML( "</div><!-- editOptions -->\n" );
3499
	}
3500
3501
	/**
3502
	 * Show an edit conflict. textbox1 is already shown in showEditForm().
3503
	 * If you want to use another entry point to this function, be careful.
3504
	 */
3505
	protected function showConflict() {
3506
		global $wgOut;
3507
3508
		if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3509
			$stats = $wgOut->getContext()->getStats();
3510
			$stats->increment( 'edit.failures.conflict' );
3511
			// Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
3512
			if (
3513
				$this->mTitle->getNamespace() >= NS_MAIN &&
3514
				$this->mTitle->getNamespace() <= NS_CATEGORY_TALK
3515
			) {
3516
				$stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
3517
			}
3518
3519
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3520
3521
			$content1 = $this->toEditContent( $this->textbox1 );
3522
			$content2 = $this->toEditContent( $this->textbox2 );
3523
3524
			$handler = ContentHandler::getForModelID( $this->contentModel );
3525
			$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3526
			$de->setContent( $content2, $content1 );
0 ignored issues
show
Bug introduced by
It seems like $content2 defined by $this->toEditContent($this->textbox2) on line 3522 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 3521 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...
3527
			$de->showDiff(
3528
				wfMessage( 'yourtext' )->parse(),
3529
				wfMessage( 'storedversion' )->text()
3530
			);
3531
3532
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3533
			$this->showTextbox2();
3534
		}
3535
	}
3536
3537
	/**
3538
	 * @return string
3539
	 */
3540
	public function getCancelLink() {
3541
		$cancelParams = [];
3542
		if ( !$this->isConflict && $this->oldid > 0 ) {
3543
			$cancelParams['oldid'] = $this->oldid;
3544
		} elseif ( $this->getContextTitle()->isRedirect() ) {
3545
			$cancelParams['redirect'] = 'no';
3546
		}
3547
		$attrs = [ 'id' => 'mw-editform-cancel' ];
3548
3549
		return Linker::linkKnown(
3550
			$this->getContextTitle(),
3551
			wfMessage( 'cancel' )->parse(),
3552
			Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3553
			$cancelParams
3554
		);
3555
	}
3556
3557
	/**
3558
	 * Returns the URL to use in the form's action attribute.
3559
	 * This is used by EditPage subclasses when simply customizing the action
3560
	 * variable in the constructor is not enough. This can be used when the
3561
	 * EditPage lives inside of a Special page rather than a custom page action.
3562
	 *
3563
	 * @param Title $title Title object for which is being edited (where we go to for &action= links)
3564
	 * @return string
3565
	 */
3566
	protected function getActionURL( Title $title ) {
3567
		return $title->getLocalURL( [ 'action' => $this->action ] );
3568
	}
3569
3570
	/**
3571
	 * Check if a page was deleted while the user was editing it, before submit.
3572
	 * Note that we rely on the logging table, which hasn't been always there,
3573
	 * but that doesn't matter, because this only applies to brand new
3574
	 * deletes.
3575
	 * @return bool
3576
	 */
3577
	protected function wasDeletedSinceLastEdit() {
3578
		if ( $this->deletedSinceEdit !== null ) {
3579
			return $this->deletedSinceEdit;
3580
		}
3581
3582
		$this->deletedSinceEdit = false;
3583
3584
		if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3585
			$this->lastDelete = $this->getLastDelete();
3586
			if ( $this->lastDelete ) {
3587
				$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3588
				if ( $deleteTime > $this->starttime ) {
3589
					$this->deletedSinceEdit = true;
3590
				}
3591
			}
3592
		}
3593
3594
		return $this->deletedSinceEdit;
3595
	}
3596
3597
	/**
3598
	 * @return bool|stdClass
3599
	 */
3600
	protected function getLastDelete() {
3601
		$dbr = wfGetDB( DB_SLAVE );
3602
		$data = $dbr->selectRow(
3603
			[ 'logging', 'user' ],
3604
			[
3605
				'log_type',
3606
				'log_action',
3607
				'log_timestamp',
3608
				'log_user',
3609
				'log_namespace',
3610
				'log_title',
3611
				'log_comment',
3612
				'log_params',
3613
				'log_deleted',
3614
				'user_name'
3615
			], [
3616
				'log_namespace' => $this->mTitle->getNamespace(),
3617
				'log_title' => $this->mTitle->getDBkey(),
3618
				'log_type' => 'delete',
3619
				'log_action' => 'delete',
3620
				'user_id=log_user'
3621
			],
3622
			__METHOD__,
3623
			[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3624
		);
3625
		// Quick paranoid permission checks...
3626
		if ( is_object( $data ) ) {
3627
			if ( $data->log_deleted & LogPage::DELETED_USER ) {
3628
				$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3629
			}
3630
3631
			if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3632
				$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3633
			}
3634
		}
3635
3636
		return $data;
3637
	}
3638
3639
	/**
3640
	 * Get the rendered text for previewing.
3641
	 * @throws MWException
3642
	 * @return string
3643
	 */
3644
	function getPreviewText() {
3645
		global $wgOut, $wgRawHtml, $wgLang;
3646
		global $wgAllowUserCss, $wgAllowUserJs;
3647
3648
		$stats = $wgOut->getContext()->getStats();
3649
3650
		if ( $wgRawHtml && !$this->mTokenOk ) {
3651
			// Could be an offsite preview attempt. This is very unsafe if
3652
			// HTML is enabled, as it could be an attack.
3653
			$parsedNote = '';
3654
			if ( $this->textbox1 !== '' ) {
3655
				// Do not put big scary notice, if previewing the empty
3656
				// string, which happens when you initially edit
3657
				// a category page, due to automatic preview-on-open.
3658
				$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3659
					wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3660
			}
3661
			$stats->increment( 'edit.failures.session_loss' );
3662
			return $parsedNote;
3663
		}
3664
3665
		$note = '';
3666
3667
		try {
3668
			$content = $this->toEditContent( $this->textbox1 );
3669
3670
			$previewHTML = '';
3671
			if ( !Hooks::run(
3672
				'AlternateEditPreview',
3673
				[ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3674
			) {
3675
				return $previewHTML;
3676
			}
3677
3678
			# provide a anchor link to the editform
3679
			$continueEditing = '<span class="mw-continue-editing">' .
3680
				'[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3681
				wfMessage( 'continue-editing' )->text() . ']]</span>';
3682
			if ( $this->mTriedSave && !$this->mTokenOk ) {
3683
				if ( $this->mTokenOkExceptSuffix ) {
3684
					$note = wfMessage( 'token_suffix_mismatch' )->plain();
3685
					$stats->increment( 'edit.failures.bad_token' );
3686
				} else {
3687
					$note = wfMessage( 'session_fail_preview' )->plain();
3688
					$stats->increment( 'edit.failures.session_loss' );
3689
				}
3690
			} elseif ( $this->incompleteForm ) {
3691
				$note = wfMessage( 'edit_form_incomplete' )->plain();
3692
				if ( $this->mTriedSave ) {
3693
					$stats->increment( 'edit.failures.incomplete_form' );
3694
				}
3695
			} else {
3696
				$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3697
			}
3698
3699
			# don't parse non-wikitext pages, show message about preview
3700
			if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3701
				if ( $this->mTitle->isCssJsSubpage() ) {
3702
					$level = 'user';
3703
				} elseif ( $this->mTitle->isCssOrJsPage() ) {
3704
					$level = 'site';
3705
				} else {
3706
					$level = false;
3707
				}
3708
3709
				if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3710
					$format = 'css';
3711
					if ( $level === 'user' && !$wgAllowUserCss ) {
3712
						$format = false;
3713
					}
3714
				} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3715
					$format = 'js';
3716
					if ( $level === 'user' && !$wgAllowUserJs ) {
3717
						$format = false;
3718
					}
3719
				} else {
3720
					$format = false;
3721
				}
3722
3723
				# Used messages to make sure grep find them:
3724
				# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3725
				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...
3726
					$note = "<div id='mw-{$level}{$format}preview'>" .
3727
						wfMessage( "{$level}{$format}preview" )->text() .
3728
						' ' . $continueEditing . "</div>";
3729
				}
3730
			}
3731
3732
			# If we're adding a comment, we need to show the
3733
			# summary as the headline
3734
			if ( $this->section === "new" && $this->summary !== "" ) {
3735
				$content = $content->addSectionHeader( $this->summary );
3736
			}
3737
3738
			$hook_args = [ $this, &$content ];
3739
			ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3740
			Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3741
3742
			$parserResult = $this->doPreviewParse( $content );
3743
			$parserOutput = $parserResult['parserOutput'];
3744
			$previewHTML = $parserResult['html'];
3745
			$this->mParserOutput = $parserOutput;
3746
			$wgOut->addParserOutputMetadata( $parserOutput );
3747
3748
			if ( count( $parserOutput->getWarnings() ) ) {
3749
				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3750
			}
3751
3752
		} catch ( MWContentSerializationException $ex ) {
3753
			$m = wfMessage(
3754
				'content-failed-to-parse',
3755
				$this->contentModel,
3756
				$this->contentFormat,
3757
				$ex->getMessage()
3758
			);
3759
			$note .= "\n\n" . $m->parse();
3760
			$previewHTML = '';
3761
		}
3762
3763
		if ( $this->isConflict ) {
3764
			$conflict = '<h2 id="mw-previewconflict">'
3765
				. wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3766
		} else {
3767
			$conflict = '<hr />';
3768
		}
3769
3770
		$previewhead = "<div class='previewnote'>\n" .
3771
			'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3772
			$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3773
3774
		$pageViewLang = $this->mTitle->getPageViewLanguage();
3775
		$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3776
			'class' => 'mw-content-' . $pageViewLang->getDir() ];
3777
		$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3778
3779
		return $previewhead . $previewHTML . $this->previewTextAfterContent;
3780
	}
3781
3782
	/**
3783
	 * Get parser options for a preview
3784
	 * @return ParserOptions
3785
	 */
3786
	protected function getPreviewParserOptions() {
3787
		$parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3788
		$parserOptions->setIsPreview( true );
3789
		$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3790
		$parserOptions->enableLimitReport();
3791
		return $parserOptions;
3792
	}
3793
3794
	/**
3795
	 * Parse the page for a preview. Subclasses may override this class, in order
3796
	 * to parse with different options, or to otherwise modify the preview HTML.
3797
	 *
3798
	 * @param Content @content The page content
3799
	 * @return Associative array with keys:
3800
	 *   - parserOutput: The ParserOutput object
3801
	 *   - html: The HTML to be displayed
3802
	 */
3803
	protected function doPreviewParse( Content $content ) {
3804
		global $wgUser;
3805
		$parserOptions = $this->getPreviewParserOptions();
3806
		$pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3807
		$scopedCallback = $parserOptions->setupFakeRevision(
3808
			$this->mTitle, $pstContent, $wgUser );
3809
		$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3810
		ScopedCallback::consume( $scopedCallback );
3811
		$parserOutput->setEditSectionTokens( false ); // no section edit links
3812
		return [
3813
			'parserOutput' => $parserOutput,
3814
			'html' => $parserOutput->getText() ];
3815
	}
3816
3817
	/**
3818
	 * @return array
3819
	 */
3820
	function getTemplates() {
3821
		if ( $this->preview || $this->section != '' ) {
3822
			$templates = [];
3823
			if ( !isset( $this->mParserOutput ) ) {
3824
				return $templates;
3825
			}
3826
			foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3827
				foreach ( array_keys( $template ) as $dbk ) {
3828
					$templates[] = Title::makeTitle( $ns, $dbk );
3829
				}
3830
			}
3831
			return $templates;
3832
		} else {
3833
			return $this->mTitle->getTemplateLinksFrom();
3834
		}
3835
	}
3836
3837
	/**
3838
	 * Shows a bulletin board style toolbar for common editing functions.
3839
	 * It can be disabled in the user preferences.
3840
	 *
3841
	 * @param Title $title Title object for the page being edited (optional)
3842
	 * @return string
3843
	 */
3844
	static function getEditToolbar( $title = null ) {
3845
		global $wgContLang, $wgOut;
3846
		global $wgEnableUploads, $wgForeignFileRepos;
3847
3848
		$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3849
		$showSignature = true;
3850
		if ( $title ) {
3851
			$showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3852
		}
3853
3854
		/**
3855
		 * $toolarray is an array of arrays each of which includes the
3856
		 * opening tag, the closing tag, optionally a sample text that is
3857
		 * inserted between the two when no selection is highlighted
3858
		 * and.  The tip text is shown when the user moves the mouse
3859
		 * over the button.
3860
		 *
3861
		 * Images are defined in ResourceLoaderEditToolbarModule.
3862
		 */
3863
		$toolarray = [
3864
			[
3865
				'id'     => 'mw-editbutton-bold',
3866
				'open'   => '\'\'\'',
3867
				'close'  => '\'\'\'',
3868
				'sample' => wfMessage( 'bold_sample' )->text(),
3869
				'tip'    => wfMessage( 'bold_tip' )->text(),
3870
			],
3871
			[
3872
				'id'     => 'mw-editbutton-italic',
3873
				'open'   => '\'\'',
3874
				'close'  => '\'\'',
3875
				'sample' => wfMessage( 'italic_sample' )->text(),
3876
				'tip'    => wfMessage( 'italic_tip' )->text(),
3877
			],
3878
			[
3879
				'id'     => 'mw-editbutton-link',
3880
				'open'   => '[[',
3881
				'close'  => ']]',
3882
				'sample' => wfMessage( 'link_sample' )->text(),
3883
				'tip'    => wfMessage( 'link_tip' )->text(),
3884
			],
3885
			[
3886
				'id'     => 'mw-editbutton-extlink',
3887
				'open'   => '[',
3888
				'close'  => ']',
3889
				'sample' => wfMessage( 'extlink_sample' )->text(),
3890
				'tip'    => wfMessage( 'extlink_tip' )->text(),
3891
			],
3892
			[
3893
				'id'     => 'mw-editbutton-headline',
3894
				'open'   => "\n== ",
3895
				'close'  => " ==\n",
3896
				'sample' => wfMessage( 'headline_sample' )->text(),
3897
				'tip'    => wfMessage( 'headline_tip' )->text(),
3898
			],
3899
			$imagesAvailable ? [
3900
				'id'     => 'mw-editbutton-image',
3901
				'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3902
				'close'  => ']]',
3903
				'sample' => wfMessage( 'image_sample' )->text(),
3904
				'tip'    => wfMessage( 'image_tip' )->text(),
3905
			] : false,
3906
			$imagesAvailable ? [
3907
				'id'     => 'mw-editbutton-media',
3908
				'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3909
				'close'  => ']]',
3910
				'sample' => wfMessage( 'media_sample' )->text(),
3911
				'tip'    => wfMessage( 'media_tip' )->text(),
3912
			] : false,
3913
			[
3914
				'id'     => 'mw-editbutton-nowiki',
3915
				'open'   => "<nowiki>",
3916
				'close'  => "</nowiki>",
3917
				'sample' => wfMessage( 'nowiki_sample' )->text(),
3918
				'tip'    => wfMessage( 'nowiki_tip' )->text(),
3919
			],
3920
			$showSignature ? [
3921
				'id'     => 'mw-editbutton-signature',
3922
				'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
3923
				'close'  => '',
3924
				'sample' => '',
3925
				'tip'    => wfMessage( 'sig_tip' )->text(),
3926
			] : false,
3927
			[
3928
				'id'     => 'mw-editbutton-hr',
3929
				'open'   => "\n----\n",
3930
				'close'  => '',
3931
				'sample' => '',
3932
				'tip'    => wfMessage( 'hr_tip' )->text(),
3933
			]
3934
		];
3935
3936
		$script = 'mw.loader.using("mediawiki.toolbar", function () {';
3937
		foreach ( $toolarray as $tool ) {
3938
			if ( !$tool ) {
3939
				continue;
3940
			}
3941
3942
			$params = [
3943
				// Images are defined in ResourceLoaderEditToolbarModule
3944
				false,
3945
				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3946
				// Older browsers show a "speedtip" type message only for ALT.
3947
				// Ideally these should be different, realistically they
3948
				// probably don't need to be.
3949
				$tool['tip'],
3950
				$tool['open'],
3951
				$tool['close'],
3952
				$tool['sample'],
3953
				$tool['id'],
3954
			];
3955
3956
			$script .= Xml::encodeJsCall(
3957
				'mw.toolbar.addButton',
3958
				$params,
3959
				ResourceLoader::inDebugMode()
3960
			);
3961
		}
3962
3963
		$script .= '});';
3964
		$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3965
3966
		$toolbar = '<div id="toolbar"></div>';
3967
3968
		Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3969
3970
		return $toolbar;
3971
	}
3972
3973
	/**
3974
	 * Returns an array of html code of the following checkboxes:
3975
	 * minor and watch
3976
	 *
3977
	 * @param int $tabindex Current tabindex
3978
	 * @param array $checked Array of checkbox => bool, where bool indicates the checked
3979
	 *                 status of the checkbox
3980
	 *
3981
	 * @return array
3982
	 */
3983
	public function getCheckboxes( &$tabindex, $checked ) {
3984
		global $wgUser, $wgUseMediaWikiUIEverywhere;
3985
3986
		$checkboxes = [];
3987
3988
		// don't show the minor edit checkbox if it's a new page or section
3989
		if ( !$this->isNew ) {
3990
			$checkboxes['minor'] = '';
3991
			$minorLabel = wfMessage( 'minoredit' )->parse();
3992 View Code Duplication
			if ( $wgUser->isAllowed( 'minoredit' ) ) {
3993
				$attribs = [
3994
					'tabindex' => ++$tabindex,
3995
					'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3996
					'id' => 'wpMinoredit',
3997
				];
3998
				$minorEditHtml =
3999
					Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
4000
					"&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
4001
					Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
4002
					">{$minorLabel}</label>";
4003
4004
				if ( $wgUseMediaWikiUIEverywhere ) {
4005
					$checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4006
						$minorEditHtml .
4007
					Html::closeElement( 'div' );
4008
				} else {
4009
					$checkboxes['minor'] = $minorEditHtml;
4010
				}
4011
			}
4012
		}
4013
4014
		$watchLabel = wfMessage( 'watchthis' )->parse();
4015
		$checkboxes['watch'] = '';
4016 View Code Duplication
		if ( $wgUser->isLoggedIn() ) {
4017
			$attribs = [
4018
				'tabindex' => ++$tabindex,
4019
				'accesskey' => wfMessage( 'accesskey-watch' )->text(),
4020
				'id' => 'wpWatchthis',
4021
			];
4022
			$watchThisHtml =
4023
				Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
4024
				"&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
4025
				Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
4026
				">{$watchLabel}</label>";
4027
			if ( $wgUseMediaWikiUIEverywhere ) {
4028
				$checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4029
					$watchThisHtml .
4030
					Html::closeElement( 'div' );
4031
			} else {
4032
				$checkboxes['watch'] = $watchThisHtml;
4033
			}
4034
		}
4035
		Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
4036
		return $checkboxes;
4037
	}
4038
4039
	/**
4040
	 * Returns an array of html code of the following buttons:
4041
	 * save, diff, preview and live
4042
	 *
4043
	 * @param int $tabindex Current tabindex
4044
	 *
4045
	 * @return array
4046
	 */
4047
	public function getEditButtons( &$tabindex ) {
4048
		$buttons = [];
4049
4050
		$attribs = [
4051
			'id' => 'wpSave',
4052
			'name' => 'wpSave',
4053
			'tabindex' => ++$tabindex,
4054
		] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4055
		$buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
4056
			$attribs, [ 'mw-ui-constructive' ] );
4057
4058
		++$tabindex; // use the same for preview and live preview
4059
		$attribs = [
4060
			'id' => 'wpPreview',
4061
			'name' => 'wpPreview',
4062
			'tabindex' => $tabindex,
4063
		] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4064
		$buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4065
			$attribs );
4066
		$buttons['live'] = '';
4067
4068
		$attribs = [
4069
			'id' => 'wpDiff',
4070
			'name' => 'wpDiff',
4071
			'tabindex' => ++$tabindex,
4072
		] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4073
		$buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4074
			$attribs );
4075
4076
		Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4077
		return $buttons;
4078
	}
4079
4080
	/**
4081
	 * Creates a basic error page which informs the user that
4082
	 * they have attempted to edit a nonexistent section.
4083
	 */
4084
	function noSuchSectionPage() {
4085
		global $wgOut;
4086
4087
		$wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4088
4089
		$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4090
		Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4091
		$wgOut->addHTML( $res );
4092
4093
		$wgOut->returnToMain( false, $this->mTitle );
4094
	}
4095
4096
	/**
4097
	 * Show "your edit contains spam" page with your diff and text
4098
	 *
4099
	 * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
4100
	 */
4101
	public function spamPageWithContent( $match = false ) {
4102
		global $wgOut, $wgLang;
4103
		$this->textbox2 = $this->textbox1;
4104
4105
		if ( is_array( $match ) ) {
4106
			$match = $wgLang->listToText( $match );
4107
		}
4108
		$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4109
4110
		$wgOut->addHTML( '<div id="spamprotected">' );
4111
		$wgOut->addWikiMsg( 'spamprotectiontext' );
4112
		if ( $match ) {
4113
			$wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4114
		}
4115
		$wgOut->addHTML( '</div>' );
4116
4117
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4118
		$this->showDiff();
4119
4120
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4121
		$this->showTextbox2();
4122
4123
		$wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4124
	}
4125
4126
	/**
4127
	 * Check if the browser is on a blacklist of user-agents known to
4128
	 * mangle UTF-8 data on form submission. Returns true if Unicode
4129
	 * should make it through, false if it's known to be a problem.
4130
	 * @return bool
4131
	 */
4132
	private function checkUnicodeCompliantBrowser() {
4133
		global $wgBrowserBlackList, $wgRequest;
4134
4135
		$currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4136
		if ( $currentbrowser === false ) {
4137
			// No User-Agent header sent? Trust it by default...
4138
			return true;
4139
		}
4140
4141
		foreach ( $wgBrowserBlackList as $browser ) {
4142
			if ( preg_match( $browser, $currentbrowser ) ) {
4143
				return false;
4144
			}
4145
		}
4146
		return true;
4147
	}
4148
4149
	/**
4150
	 * Filter an input field through a Unicode de-armoring process if it
4151
	 * came from an old browser with known broken Unicode editing issues.
4152
	 *
4153
	 * @param WebRequest $request
4154
	 * @param string $field
4155
	 * @return string
4156
	 */
4157
	protected function safeUnicodeInput( $request, $field ) {
4158
		$text = rtrim( $request->getText( $field ) );
4159
		return $request->getBool( 'safemode' )
4160
			? $this->unmakeSafe( $text )
4161
			: $text;
4162
	}
4163
4164
	/**
4165
	 * Filter an output field through a Unicode armoring process if it is
4166
	 * going to an old browser with known broken Unicode editing issues.
4167
	 *
4168
	 * @param string $text
4169
	 * @return string
4170
	 */
4171
	protected function safeUnicodeOutput( $text ) {
4172
		return $this->checkUnicodeCompliantBrowser()
4173
			? $text
4174
			: $this->makesafe( $text );
4175
	}
4176
4177
	/**
4178
	 * A number of web browsers are known to corrupt non-ASCII characters
4179
	 * in a UTF-8 text editing environment. To protect against this,
4180
	 * detected browsers will be served an armored version of the text,
4181
	 * with non-ASCII chars converted to numeric HTML character references.
4182
	 *
4183
	 * Preexisting such character references will have a 0 added to them
4184
	 * to ensure that round-trips do not alter the original data.
4185
	 *
4186
	 * @param string $invalue
4187
	 * @return string
4188
	 */
4189
	private function makeSafe( $invalue ) {
4190
		// Armor existing references for reversibility.
4191
		$invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4192
4193
		$bytesleft = 0;
4194
		$result = "";
4195
		$working = 0;
4196
		$valueLength = strlen( $invalue );
4197
		for ( $i = 0; $i < $valueLength; $i++ ) {
4198
			$bytevalue = ord( $invalue[$i] );
4199
			if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4200
				$result .= chr( $bytevalue );
4201
				$bytesleft = 0;
4202
			} elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4203
				$working = $working << 6;
4204
				$working += ( $bytevalue & 0x3F );
4205
				$bytesleft--;
4206
				if ( $bytesleft <= 0 ) {
4207
					$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4208
				}
4209
			} elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4210
				$working = $bytevalue & 0x1F;
4211
				$bytesleft = 1;
4212
			} elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4213
				$working = $bytevalue & 0x0F;
4214
				$bytesleft = 2;
4215
			} else { // 1111 0xxx
4216
				$working = $bytevalue & 0x07;
4217
				$bytesleft = 3;
4218
			}
4219
		}
4220
		return $result;
4221
	}
4222
4223
	/**
4224
	 * Reverse the previously applied transliteration of non-ASCII characters
4225
	 * back to UTF-8. Used to protect data from corruption by broken web browsers
4226
	 * as listed in $wgBrowserBlackList.
4227
	 *
4228
	 * @param string $invalue
4229
	 * @return string
4230
	 */
4231
	private function unmakeSafe( $invalue ) {
4232
		$result = "";
4233
		$valueLength = strlen( $invalue );
4234
		for ( $i = 0; $i < $valueLength; $i++ ) {
4235
			if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4236
				$i += 3;
4237
				$hexstring = "";
4238
				do {
4239
					$hexstring .= $invalue[$i];
4240
					$i++;
4241
				} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4242
4243
				// Do some sanity checks. These aren't needed for reversibility,
4244
				// but should help keep the breakage down if the editor
4245
				// breaks one of the entities whilst editing.
4246
				if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4247
					$codepoint = hexdec( $hexstring );
4248
					$result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4249
				} else {
4250
					$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4251
				}
4252
			} else {
4253
				$result .= substr( $invalue, $i, 1 );
4254
			}
4255
		}
4256
		// reverse the transform that we made for reversibility reasons.
4257
		return strtr( $result, [ "&#x0" => "&#x" ] );
4258
	}
4259
}
4260