Completed
Branch master (b92a94)
by
unknown
34:34
created

EditPage::getContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
rs 10
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
	 * @var IContextSource
406
	 */
407
	protected $context;
408
409
	/**
410
	 * @param Article $article
411
	 */
412
	public function __construct( Article $article ) {
413
		$this->mArticle = $article;
414
		$this->page = $article->getPage(); // model object
415
		$this->mTitle = $article->getTitle();
416
		$this->context = $article->getContext();
417
418
		$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...
419
420
		$handler = ContentHandler::getForModelID( $this->contentModel );
421
		$this->contentFormat = $handler->getDefaultFormat();
422
	}
423
424
	/**
425
	 * @return Article
426
	 */
427
	public function getArticle() {
428
		return $this->mArticle;
429
	}
430
431
	/**
432
	 * @since 1.28
433
	 * @return IContextSource
434
	 */
435
	public function getContext() {
436
		return $this->context;
437
	}
438
439
	/**
440
	 * @since 1.19
441
	 * @return Title
442
	 */
443
	public function getTitle() {
444
		return $this->mTitle;
445
	}
446
447
	/**
448
	 * Set the context Title object
449
	 *
450
	 * @param Title|null $title Title object or null
451
	 */
452
	public function setContextTitle( $title ) {
453
		$this->mContextTitle = $title;
454
	}
455
456
	/**
457
	 * Get the context title object.
458
	 * If not set, $wgTitle will be returned. This behavior might change in
459
	 * the future to return $this->mTitle instead.
460
	 *
461
	 * @return Title
462
	 */
463
	public function getContextTitle() {
464
		if ( is_null( $this->mContextTitle ) ) {
465
			global $wgTitle;
466
			return $wgTitle;
467
		} else {
468
			return $this->mContextTitle;
469
		}
470
	}
471
472
	/**
473
	 * Returns if the given content model is editable.
474
	 *
475
	 * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
476
	 * @return bool
477
	 * @throws MWException If $modelId has no known handler
478
	 */
479
	public function isSupportedContentModel( $modelId ) {
480
		return $this->enableApiEditOverride === true ||
481
			ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
482
	}
483
484
	/**
485
	 * Allow editing of content that supports API direct editing, but not general
486
	 * direct editing. Set to false by default.
487
	 *
488
	 * @param bool $enableOverride
489
	 */
490
	public function setApiEditOverride( $enableOverride ) {
491
		$this->enableApiEditOverride = $enableOverride;
492
	}
493
494
	function submit() {
495
		$this->edit();
496
	}
497
498
	/**
499
	 * This is the function that gets called for "action=edit". It
500
	 * sets up various member variables, then passes execution to
501
	 * another function, usually showEditForm()
502
	 *
503
	 * The edit form is self-submitting, so that when things like
504
	 * preview and edit conflicts occur, we get the same form back
505
	 * with the extra stuff added.  Only when the final submission
506
	 * is made and all is well do we actually save and redirect to
507
	 * the newly-edited page.
508
	 */
509
	function edit() {
510
		// Allow extensions to modify/prevent this form or submission
511
		if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
512
			return;
513
		}
514
515
		wfDebug( __METHOD__ . ": enter\n" );
516
517
		$request = $this->context->getRequest();
518
		$out = $this->context->getOutput();
519
		// If they used redlink=1 and the page exists, redirect to the main article
520
		if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
521
			$out->redirect( $this->mTitle->getFullURL() );
522
			return;
523
		}
524
525
		$this->importFormData( $request );
526
		$this->firsttime = false;
527
528
		if ( wfReadOnly() && $this->save ) {
529
			// Force preview
530
			$this->save = false;
531
			$this->preview = true;
532
		}
533
534
		if ( $this->save ) {
535
			$this->formtype = 'save';
536
		} elseif ( $this->preview ) {
537
			$this->formtype = 'preview';
538
		} elseif ( $this->diff ) {
539
			$this->formtype = 'diff';
540
		} else { # First time through
541
			$this->firsttime = true;
542
			if ( $this->previewOnOpen() ) {
543
				$this->formtype = 'preview';
544
			} else {
545
				$this->formtype = 'initial';
546
			}
547
		}
548
549
		$permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
550
		if ( $permErrors ) {
551
			wfDebug( __METHOD__ . ": User can't edit\n" );
552
			// Auto-block user's IP if the account was "hard" blocked
553
			if ( !wfReadOnly() ) {
554
				$user = $this->context->getUser();
555
				DeferredUpdates::addCallableUpdate( function () use ( $user ) {
556
					$user->spreadAnyEditBlock();
557
				} );
558
			}
559
			$this->displayPermissionsError( $permErrors );
560
561
			return;
562
		}
563
564
		$revision = $this->mArticle->getRevisionFetched();
565
		// Disallow editing revisions with content models different from the current one
566
		if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
567
			$this->displayViewSourcePage(
568
				$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...
569
				wfMessage(
570
					'contentmodelediterror',
571
					$revision->getContentModel(),
572
					$this->contentModel
573
				)->plain()
574
			);
575
			return;
576
		}
577
578
		$this->isConflict = false;
579
		// css / js subpages of user pages get a special treatment
580
		$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
581
		$this->isCssSubpage = $this->mTitle->isCssSubpage();
582
		$this->isJsSubpage = $this->mTitle->isJsSubpage();
583
		// @todo FIXME: Silly assignment.
584
		$this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
585
586
		# Show applicable editing introductions
587
		if ( $this->formtype == 'initial' || $this->firsttime ) {
588
			$this->showIntro();
589
		}
590
591
		# Attempt submission here.  This will check for edit conflicts,
592
		# and redundantly check for locked database, blocked IPs, etc.
593
		# that edit() already checked just in case someone tries to sneak
594
		# in the back door with a hand-edited submission URL.
595
596
		if ( 'save' == $this->formtype ) {
597
			$resultDetails = null;
598
			$status = $this->attemptSave( $resultDetails );
599
			if ( !$this->handleStatus( $status, $resultDetails ) ) {
600
				return;
601
			}
602
		}
603
604
		# First time through: get contents, set time for conflict
605
		# checking, etc.
606
		if ( 'initial' == $this->formtype || $this->firsttime ) {
607
			if ( $this->initialiseForm() === false ) {
608
				$this->noSuchSectionPage();
609
				return;
610
			}
611
612
			if ( !$this->mTitle->getArticleID() ) {
613
				Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
614
			} else {
615
				Hooks::run( 'EditFormInitialText', [ $this ] );
616
			}
617
618
		}
619
620
		$this->showEditForm();
621
	}
622
623
	/**
624
	 * @param string $rigor Same format as Title::getUserPermissionErrors()
625
	 * @return array
626
	 */
627
	protected function getEditPermissionErrors( $rigor = 'secure' ) {
628
		$user = $this->context->getUser();
629
		$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
630
		# Can this title be created?
631
		if ( !$this->mTitle->exists() ) {
632
			$permErrors = array_merge(
633
				$permErrors,
634
				wfArrayDiff2(
635
					$this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
636
					$permErrors
637
				)
638
			);
639
		}
640
		# Ignore some permissions errors when a user is just previewing/viewing diffs
641
		$remove = [];
642
		foreach ( $permErrors as $error ) {
643
			if ( ( $this->preview || $this->diff )
644
				&& ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
645
			) {
646
				$remove[] = $error;
647
			}
648
		}
649
		$permErrors = wfArrayDiff2( $permErrors, $remove );
650
651
		return $permErrors;
652
	}
653
654
	/**
655
	 * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
656
	 * but with the following differences:
657
	 * - If redlink=1, the user will be redirected to the page
658
	 * - If there is content to display or the error occurs while either saving,
659
	 *   previewing or showing the difference, it will be a
660
	 *   "View source for ..." page displaying the source code after the error message.
661
	 *
662
	 * @since 1.19
663
	 * @param array $permErrors Array of permissions errors, as returned by
664
	 *    Title::getUserPermissionsErrors().
665
	 * @throws PermissionsError
666
	 */
667
	protected function displayPermissionsError( array $permErrors ) {
668
		$out = $this->context->getOutput();
669
		if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
670
			// The edit page was reached via a red link.
671
			// Redirect to the article page and let them click the edit tab if
672
			// they really want a permission error.
673
			$out->redirect( $this->mTitle->getFullURL() );
674
			return;
675
		}
676
677
		$content = $this->getContentObject();
678
679
		# Use the normal message if there's nothing to display
680
		if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
681
			$action = $this->mTitle->exists() ? 'edit' :
682
				( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
683
			throw new PermissionsError( $action, $permErrors );
684
		}
685
686
		$this->displayViewSourcePage(
687
			$content,
0 ignored issues
show
Bug introduced by
It seems like $content defined by $this->getContentObject() on line 677 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...
688
			$out->formatPermissionsErrorMessage( $permErrors, 'edit' )
689
		);
690
	}
691
692
	/**
693
	 * Display a read-only View Source page
694
	 * @param Content $content content object
695
	 * @param string $errorMessage additional wikitext error message to display
696
	 */
697
	protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
698
		$out = $this->context->getOutput();
699
		Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
700
701
		$out->setRobotPolicy( 'noindex,nofollow' );
702
		$out->setPageTitle( wfMessage(
703
			'viewsource-title',
704
			$this->getContextTitle()->getPrefixedText()
705
		) );
706
		$out->addBacklinkSubtitle( $this->getContextTitle() );
707
		$out->addHTML( $this->editFormPageTop );
708
		$out->addHTML( $this->editFormTextTop );
709
710
		if ( $errorMessage !== '' ) {
711
			$out->addWikiText( $errorMessage );
712
			$out->addHTML( "<hr />\n" );
713
		}
714
715
		# If the user made changes, preserve them when showing the markup
716
		# (This happens when a user is blocked during edit, for instance)
717
		if ( !$this->firsttime ) {
718
			$text = $this->textbox1;
719
			$out->addWikiMsg( 'viewyourtext' );
720
		} else {
721
			try {
722
				$text = $this->toEditText( $content );
723
			} catch ( MWException $e ) {
724
				# Serialize using the default format if the content model is not supported
725
				# (e.g. for an old revision with a different model)
726
				$text = $content->serialize();
727
			}
728
			$out->addWikiMsg( 'viewsourcetext' );
729
		}
730
731
		$out->addHTML( $this->editFormTextBeforeContent );
732
		$this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
733
		$out->addHTML( $this->editFormTextAfterContent );
734
735
		$out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
736
			Linker::formatTemplates( $this->getTemplates() ) ) );
737
738
		$out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
739
740
		$out->addHTML( $this->editFormTextBottom );
741
		if ( $this->mTitle->exists() ) {
742
			$out->returnToMain( null, $this->mTitle );
743
		}
744
	}
745
746
	/**
747
	 * Should we show a preview when the edit form is first shown?
748
	 *
749
	 * @return bool
750
	 */
751
	protected function previewOnOpen() {
752
		global $wgPreviewOnOpenNamespaces;
753
		$request = $this->context->getRequest();
754
		if ( $request->getVal( 'preview' ) == 'yes' ) {
755
			// Explicit override from request
756
			return true;
757
		} elseif ( $request->getVal( 'preview' ) == 'no' ) {
758
			// Explicit override from request
759
			return false;
760
		} elseif ( $this->section == 'new' ) {
761
			// Nothing *to* preview for new sections
762
			return false;
763
		} elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
764
			&& $this->context->getUser()->getOption( 'previewonfirst' )
765
		) {
766
			// Standard preference behavior
767
			return true;
768
		} elseif ( !$this->mTitle->exists()
769
			&& isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
770
			&& $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
771
		) {
772
			// Categories are special
773
			return true;
774
		} else {
775
			return false;
776
		}
777
	}
778
779
	/**
780
	 * Checks whether the user entered a skin name in uppercase,
781
	 * e.g. "User:Example/Monobook.css" instead of "monobook.css"
782
	 *
783
	 * @return bool
784
	 */
785
	protected function isWrongCaseCssJsPage() {
786
		if ( $this->mTitle->isCssJsSubpage() ) {
787
			$name = $this->mTitle->getSkinFromCssJsSubpage();
788
			$skins = array_merge(
789
				array_keys( Skin::getSkinNames() ),
790
				[ 'common' ]
791
			);
792
			return !in_array( $name, $skins )
793
				&& in_array( strtolower( $name ), $skins );
794
		} else {
795
			return false;
796
		}
797
	}
798
799
	/**
800
	 * Returns whether section editing is supported for the current page.
801
	 * Subclasses may override this to replace the default behavior, which is
802
	 * to check ContentHandler::supportsSections.
803
	 *
804
	 * @return bool True if this edit page supports sections, false otherwise.
805
	 */
806
	protected function isSectionEditSupported() {
807
		$contentHandler = ContentHandler::getForTitle( $this->mTitle );
808
		return $contentHandler->supportsSections();
809
	}
810
811
	/**
812
	 * This function collects the form data and uses it to populate various member variables.
813
	 * @param WebRequest $request
814
	 * @throws ErrorPageError
815
	 */
816
	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...
817
		global $wgContLang;
818
819
		# Section edit can come from either the form or a link
820
		$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
821
822
		if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
823
			throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
824
		}
825
826
		$this->isNew = !$this->mTitle->exists() || $this->section == 'new';
827
828
		if ( $request->wasPosted() ) {
829
			# These fields need to be checked for encoding.
830
			# Also remove trailing whitespace, but don't remove _initial_
831
			# whitespace from the text boxes. This may be significant formatting.
832
			$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
833
			if ( !$request->getCheck( 'wpTextbox2' ) ) {
834
				// Skip this if wpTextbox2 has input, it indicates that we came
835
				// from a conflict page with raw page text, not a custom form
836
				// modified by subclasses
837
				$textbox1 = $this->importContentFormData( $request );
838
				if ( $textbox1 !== null ) {
839
					$this->textbox1 = $textbox1;
840
				}
841
			}
842
843
			# Truncate for whole multibyte characters
844
			$this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
845
846
			# If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
847
			# header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
848
			# section titles.
849
			$this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
850
851
			# Treat sectiontitle the same way as summary.
852
			# Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
853
			# currently doing double duty as both edit summary and section title. Right now this
854
			# is just to allow API edits to work around this limitation, but this should be
855
			# incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
856
			$this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
857
			$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
858
859
			$this->edittime = $request->getVal( 'wpEdittime' );
860
			$this->editRevId = $request->getIntOrNull( 'editRevId' );
861
			$this->starttime = $request->getVal( 'wpStarttime' );
862
863
			$undidRev = $request->getInt( 'wpUndidRevision' );
864
			if ( $undidRev ) {
865
				$this->undidRev = $undidRev;
866
			}
867
868
			$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...
869
870
			if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
871
				// wpTextbox1 field is missing, possibly due to being "too big"
872
				// according to some filter rules such as Suhosin's setting for
873
				// suhosin.request.max_value_length (d'oh)
874
				$this->incompleteForm = true;
875
			} else {
876
				// If we receive the last parameter of the request, we can fairly
877
				// claim the POST request has not been truncated.
878
879
				// TODO: softened the check for cutover.  Once we determine
880
				// that it is safe, we should complete the transition by
881
				// removing the "edittime" clause.
882
				$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...
883
					&& is_null( $this->edittime ) );
884
			}
885
			if ( $this->incompleteForm ) {
886
				# If the form is incomplete, force to preview.
887
				wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
888
				wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
889
				$this->preview = true;
890
			} else {
891
				$this->preview = $request->getCheck( 'wpPreview' );
892
				$this->diff = $request->getCheck( 'wpDiff' );
893
894
				// Remember whether a save was requested, so we can indicate
895
				// if we forced preview due to session failure.
896
				$this->mTriedSave = !$this->preview;
897
898
				if ( $this->tokenOk( $request ) ) {
899
					# Some browsers will not report any submit button
900
					# if the user hits enter in the comment box.
901
					# The unmarked state will be assumed to be a save,
902
					# if the form seems otherwise complete.
903
					wfDebug( __METHOD__ . ": Passed token check.\n" );
904
				} elseif ( $this->diff ) {
905
					# Failed token check, but only requested "Show Changes".
906
					wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
907
				} else {
908
					# Page might be a hack attempt posted from
909
					# an external site. Preview instead of saving.
910
					wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
911
					$this->preview = true;
912
				}
913
			}
914
			$this->save = !$this->preview && !$this->diff;
915
			if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
916
				$this->edittime = null;
917
			}
918
919
			if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
920
				$this->starttime = null;
921
			}
922
923
			$this->recreate = $request->getCheck( 'wpRecreate' );
924
925
			$this->minoredit = $request->getCheck( 'wpMinoredit' );
926
			$this->watchthis = $request->getCheck( 'wpWatchthis' );
927
928
			# Don't force edit summaries when a user is editing their own user or talk page
929
			$user = $this->context->getUser();
930
			if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
931
				&& $this->mTitle->getText() == $user->getName()
932
			) {
933
				$this->allowBlankSummary = true;
934
			} else {
935
				$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
936
					|| !$user->getOption( 'forceeditsummary' );
937
			}
938
939
			$this->autoSumm = $request->getText( 'wpAutoSummary' );
940
941
			$this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
942
			$this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
943
944
			$changeTags = $request->getVal( 'wpChangeTags' );
945
			if ( is_null( $changeTags ) || $changeTags === '' ) {
946
				$this->changeTags = [];
947
			} else {
948
				$this->changeTags = array_filter( array_map( 'trim', explode( ',',
949
					$changeTags ) ) );
950
			}
951
		} else {
952
			# Not a posted form? Start with nothing.
953
			wfDebug( __METHOD__ . ": Not a posted form.\n" );
954
			$this->textbox1 = '';
955
			$this->summary = '';
956
			$this->sectiontitle = '';
957
			$this->edittime = '';
958
			$this->editRevId = null;
959
			$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...
960
			$this->edit = false;
961
			$this->preview = false;
962
			$this->save = false;
963
			$this->diff = false;
964
			$this->minoredit = false;
965
			// Watch may be overridden by request parameters
966
			$this->watchthis = $request->getBool( 'watchthis', false );
967
			$this->recreate = false;
968
969
			// When creating a new section, we can preload a section title by passing it as the
970
			// preloadtitle parameter in the URL (Bug 13100)
971
			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...
972
				$this->sectiontitle = $request->getVal( 'preloadtitle' );
973
				// Once wpSummary isn't being use for setting section titles, we should delete this.
974
				$this->summary = $request->getVal( 'preloadtitle' );
975
			} 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...
976
				$this->summary = $request->getText( 'summary' );
977
				if ( $this->summary !== '' ) {
978
					$this->hasPresetSummary = true;
979
				}
980
			}
981
982
			if ( $request->getVal( 'minor' ) ) {
983
				$this->minoredit = true;
984
			}
985
		}
986
987
		$this->oldid = $request->getInt( 'oldid' );
988
		$this->parentRevId = $request->getInt( 'parentRevId' );
989
990
		$this->bot = $request->getBool( 'bot', true );
991
		$this->nosummary = $request->getBool( 'nosummary' );
992
993
		// May be overridden by revision.
994
		$this->contentModel = $request->getText( 'model', $this->contentModel );
995
		// May be overridden by revision.
996
		$this->contentFormat = $request->getText( 'format', $this->contentFormat );
997
998
		if ( !ContentHandler::getForModelID( $this->contentModel )
999
			->isSupportedFormat( $this->contentFormat )
1000
		) {
1001
			throw new ErrorPageError(
1002
				'editpage-notsupportedcontentformat-title',
1003
				'editpage-notsupportedcontentformat-text',
1004
				[ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ]
1005
			);
1006
		}
1007
1008
		/**
1009
		 * @todo Check if the desired model is allowed in this namespace, and if
1010
		 *   a transition from the page's current model to the new model is
1011
		 *   allowed.
1012
		 */
1013
1014
		$this->editintro = $request->getText( 'editintro',
1015
			// Custom edit intro for new sections
1016
			$this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1017
1018
		// Allow extensions to modify form data
1019
		Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1020
1021
	}
1022
1023
	/**
1024
	 * Subpage overridable method for extracting the page content data from the
1025
	 * posted form to be placed in $this->textbox1, if using customized input
1026
	 * this method should be overridden and return the page text that will be used
1027
	 * for saving, preview parsing and so on...
1028
	 *
1029
	 * @param WebRequest $request
1030
	 * @return string|null
1031
	 */
1032
	protected function importContentFormData( &$request ) {
1033
		return; // Don't do anything, EditPage already extracted wpTextbox1
1034
	}
1035
1036
	/**
1037
	 * Initialise form fields in the object
1038
	 * Called on the first invocation, e.g. when a user clicks an edit link
1039
	 * @return bool If the requested section is valid
1040
	 */
1041
	function initialiseForm() {
1042
		$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...
1043
		$this->editRevId = $this->page->getLatest();
1044
1045
		$content = $this->getContentObject( false ); # TODO: track content object?!
1046
		if ( $content === false ) {
1047
			return false;
1048
		}
1049
		$this->textbox1 = $this->toEditText( $content );
1050
		$user = $this->context->getUser();
1051
1052
		// activate checkboxes if user wants them to be always active
1053
		# Sort out the "watch" checkbox
1054
		if ( $user->getOption( 'watchdefault' ) ) {
1055
			# Watch all edits
1056
			$this->watchthis = true;
1057
		} elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1058
			# Watch creations
1059
			$this->watchthis = true;
1060
		} elseif ( $user->isWatched( $this->mTitle ) ) {
1061
			# Already watched
1062
			$this->watchthis = true;
1063
		}
1064
		if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1065
			$this->minoredit = true;
1066
		}
1067
		if ( $this->textbox1 === false ) {
1068
			return false;
1069
		}
1070
		return true;
1071
	}
1072
1073
	/**
1074
	 * @param Content|null $def_content The default value to return
1075
	 *
1076
	 * @return Content|null Content on success, $def_content for invalid sections
1077
	 *
1078
	 * @since 1.21
1079
	 */
1080
	protected function getContentObject( $def_content = null ) {
1081
		global $wgContLang;
1082
1083
		$content = false;
1084
		$request = $this->context->getRequest();
1085
		$user = $this->context->getUser();
1086
1087
		// For message page not locally set, use the i18n message.
1088
		// For other non-existent articles, use preload text if any.
1089
		if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1090
			if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1091
				# If this is a system message, get the default text.
1092
				$msg = $this->mTitle->getDefaultMessageText();
1093
1094
				$content = $this->toEditContent( $msg );
1095
			}
1096
			if ( $content === false ) {
1097
				# If requested, preload some text.
1098
				$preload = $request->getVal( 'preload',
1099
					// Custom preload text for new sections
1100
					$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1101
				$params = $request->getArray( 'preloadparams', [] );
1102
1103
				$content = $this->getPreloadedContent( $preload, $params );
0 ignored issues
show
Bug introduced by
It seems like $params defined by $request->getArray('preloadparams', array()) on line 1101 can also be of type null; however, EditPage::getPreloadedContent() does only seem to accept array, 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...
1104
			}
1105
		// For existing pages, get text based on "undo" or section parameters.
1106
		} else {
1107
			if ( $this->section != '' ) {
1108
				// Get section edit text (returns $def_text for invalid sections)
1109
				$orig = $this->getOriginalContent( $user );
1110
				$content = $orig ? $orig->getSection( $this->section ) : null;
1111
1112
				if ( !$content ) {
1113
					$content = $def_content;
1114
				}
1115
			} else {
1116
				$undoafter = $request->getInt( 'undoafter' );
1117
				$undo = $request->getInt( 'undo' );
1118
1119
				if ( $undo > 0 && $undoafter > 0 ) {
1120
					$undorev = Revision::newFromId( $undo );
1121
					$oldrev = Revision::newFromId( $undoafter );
1122
1123
					# Sanity check, make sure it's the right page,
1124
					# the revisions exist and they were not deleted.
1125
					# Otherwise, $content will be left as-is.
1126
					if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1127
						!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1128
						!$oldrev->isDeleted( Revision::DELETED_TEXT )
1129
					) {
1130
						$content = $this->page->getUndoContent( $undorev, $oldrev );
1131
1132
						if ( $content === false ) {
1133
							# Warn the user that something went wrong
1134
							$undoMsg = 'failure';
1135
						} else {
1136
							$oldContent = $this->page->getContent( Revision::RAW );
1137
							$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
1138
							$newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1139
1140
							if ( $newContent->equals( $oldContent ) ) {
1141
								# Tell the user that the undo results in no change,
1142
								# i.e. the revisions were already undone.
1143
								$undoMsg = 'nochange';
1144
								$content = false;
1145
							} else {
1146
								# Inform the user of our success and set an automatic edit summary
1147
								$undoMsg = 'success';
1148
1149
								# If we just undid one rev, use an autosummary
1150
								$firstrev = $oldrev->getNext();
1151
								if ( $firstrev && $firstrev->getId() == $undo ) {
1152
									$userText = $undorev->getUserText();
1153
									if ( $userText === '' ) {
1154
										$undoSummary = wfMessage(
1155
											'undo-summary-username-hidden',
1156
											$undo
1157
										)->inContentLanguage()->text();
1158
									} else {
1159
										$undoSummary = wfMessage(
1160
											'undo-summary',
1161
											$undo,
1162
											$userText
1163
										)->inContentLanguage()->text();
1164
									}
1165
									if ( $this->summary === '' ) {
1166
										$this->summary = $undoSummary;
1167
									} else {
1168
										$this->summary = $undoSummary . wfMessage( 'colon-separator' )
1169
											->inContentLanguage()->text() . $this->summary;
1170
									}
1171
									$this->undidRev = $undo;
1172
								}
1173
								$this->formtype = 'diff';
1174
							}
1175
						}
1176
					} else {
1177
						// Failed basic sanity checks.
1178
						// Older revisions may have been removed since the link
1179
						// was created, or we may simply have got bogus input.
1180
						$undoMsg = 'norev';
1181
					}
1182
1183
					// Messages: undo-success, undo-failure, undo-norev, undo-nochange
1184
					$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1185
					$this->editFormPageTop .= $this->context->getOutput()->parse(
1186
						"<div class=\"{$class}\">" .
1187
						wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1188
				}
1189
1190
				if ( $content === false ) {
1191
					$content = $this->getOriginalContent( $user );
1192
				}
1193
			}
1194
		}
1195
1196
		return $content;
1197
	}
1198
1199
	/**
1200
	 * Get the content of the wanted revision, without section extraction.
1201
	 *
1202
	 * The result of this function can be used to compare user's input with
1203
	 * section replaced in its context (using WikiPage::replaceSectionAtRev())
1204
	 * to the original text of the edit.
1205
	 *
1206
	 * This differs from Article::getContent() that when a missing revision is
1207
	 * encountered the result will be null and not the
1208
	 * 'missing-revision' message.
1209
	 *
1210
	 * @since 1.19
1211
	 * @param User $user The user to get the revision for
1212
	 * @return Content|null
1213
	 */
1214
	private function getOriginalContent( User $user ) {
1215
		if ( $this->section == 'new' ) {
1216
			return $this->getCurrentContent();
1217
		}
1218
		$revision = $this->mArticle->getRevisionFetched();
1219
		if ( $revision === null ) {
1220
			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...
1221
				$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...
1222
			}
1223
			$handler = ContentHandler::getForModelID( $this->contentModel );
1224
1225
			return $handler->makeEmptyContent();
1226
		}
1227
		$content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1228
		return $content;
1229
	}
1230
1231
	/**
1232
	 * Get the edit's parent revision ID
1233
	 *
1234
	 * The "parent" revision is the ancestor that should be recorded in this
1235
	 * page's revision history.  It is either the revision ID of the in-memory
1236
	 * article content, or in the case of a 3-way merge in order to rebase
1237
	 * across a recoverable edit conflict, the ID of the newer revision to
1238
	 * which we have rebased this page.
1239
	 *
1240
	 * @since 1.27
1241
	 * @return int Revision ID
1242
	 */
1243
	public function getParentRevId() {
1244
		if ( $this->parentRevId ) {
1245
			return $this->parentRevId;
1246
		} else {
1247
			return $this->mArticle->getRevIdFetched();
1248
		}
1249
	}
1250
1251
	/**
1252
	 * Get the current content of the page. This is basically similar to
1253
	 * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
1254
	 * content object is returned instead of null.
1255
	 *
1256
	 * @since 1.21
1257
	 * @return Content
1258
	 */
1259
	protected function getCurrentContent() {
1260
		$rev = $this->page->getRevision();
1261
		$content = $rev ? $rev->getContent( Revision::RAW ) : null;
1262
1263
		if ( $content === false || $content === null ) {
1264
			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...
1265
				$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...
1266
			}
1267
			$handler = ContentHandler::getForModelID( $this->contentModel );
1268
1269
			return $handler->makeEmptyContent();
1270
		} else {
1271
			// Content models should always be the same since we error
1272
			// out if they are different before this point.
1273
			$logger = LoggerFactory::getInstance( 'editpage' );
1274 View Code Duplication
			if ( $this->contentModel !== $rev->getContentModel() ) {
1275
				$logger->warning( "Overriding content model from current edit {prev} to {new}", [
1276
					'prev' => $this->contentModel,
1277
					'new' => $rev->getContentModel(),
1278
					'title' => $this->getTitle()->getPrefixedDBkey(),
1279
					'method' => __METHOD__
1280
				] );
1281
				$this->contentModel = $rev->getContentModel();
1282
			}
1283
1284
			// Given that the content models should match, the current selected
1285
			// format should be supported.
1286 View Code Duplication
			if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1287
				$logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1288
1289
					'prev' => $this->contentFormat,
1290
					'new' => $rev->getContentFormat(),
1291
					'title' => $this->getTitle()->getPrefixedDBkey(),
1292
					'method' => __METHOD__
1293
				] );
1294
				$this->contentFormat = $rev->getContentFormat();
1295
			}
1296
1297
			return $content;
1298
		}
1299
	}
1300
1301
	/**
1302
	 * Use this method before edit() to preload some content into the edit box
1303
	 *
1304
	 * @param Content $content
1305
	 *
1306
	 * @since 1.21
1307
	 */
1308
	public function setPreloadedContent( Content $content ) {
1309
		$this->mPreloadContent = $content;
1310
	}
1311
1312
	/**
1313
	 * Get the contents to be preloaded into the box, either set by
1314
	 * an earlier setPreloadText() or by loading the given page.
1315
	 *
1316
	 * @param string $preload Representing the title to preload from.
1317
	 * @param array $params Parameters to use (interface-message style) in the preloaded text
1318
	 *
1319
	 * @return Content
1320
	 *
1321
	 * @since 1.21
1322
	 */
1323
	protected function getPreloadedContent( $preload, $params = [] ) {
1324
		global $wgUser;
1325
1326
		if ( !empty( $this->mPreloadContent ) ) {
1327
			return $this->mPreloadContent;
1328
		}
1329
1330
		$handler = ContentHandler::getForModelID( $this->contentModel );
1331
1332
		if ( $preload === '' ) {
1333
			return $handler->makeEmptyContent();
1334
		}
1335
1336
		$title = Title::newFromText( $preload );
1337
		# Check for existence to avoid getting MediaWiki:Noarticletext
1338 View Code Duplication
		if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1339
			// TODO: somehow show a warning to the user!
1340
			return $handler->makeEmptyContent();
1341
		}
1342
1343
		$page = WikiPage::factory( $title );
1344
		if ( $page->isRedirect() ) {
1345
			$title = $page->getRedirectTarget();
1346
			# Same as before
1347 View Code Duplication
			if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1348
				// TODO: somehow show a warning to the user!
1349
				return $handler->makeEmptyContent();
1350
			}
1351
			$page = WikiPage::factory( $title );
1352
		}
1353
1354
		$parserOptions = ParserOptions::newFromUser( $wgUser );
1355
		$content = $page->getContent( Revision::RAW );
1356
1357
		if ( !$content ) {
1358
			// TODO: somehow show a warning to the user!
1359
			return $handler->makeEmptyContent();
1360
		}
1361
1362
		if ( $content->getModel() !== $handler->getModelID() ) {
1363
			$converted = $content->convert( $handler->getModelID() );
1364
1365
			if ( !$converted ) {
1366
				// TODO: somehow show a warning to the user!
1367
				wfDebug( "Attempt to preload incompatible content: " .
1368
					"can't convert " . $content->getModel() .
1369
					" to " . $handler->getModelID() );
1370
1371
				return $handler->makeEmptyContent();
1372
			}
1373
1374
			$content = $converted;
1375
		}
1376
1377
		return $content->preloadTransform( $title, $parserOptions, $params );
1378
	}
1379
1380
	/**
1381
	 * Make sure the form isn't faking a user's credentials.
1382
	 *
1383
	 * @param WebRequest $request
1384
	 * @return bool
1385
	 * @private
1386
	 */
1387
	function tokenOk( &$request ) {
1388
		$token = $request->getVal( 'wpEditToken' );
1389
		$user = $this->context->getUser();
1390
		$this->mTokenOk = $user->matchEditToken( $token );
1391
		$this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1392
		return $this->mTokenOk;
1393
	}
1394
1395
	/**
1396
	 * Sets post-edit cookie indicating the user just saved a particular revision.
1397
	 *
1398
	 * This uses a temporary cookie for each revision ID so separate saves will never
1399
	 * interfere with each other.
1400
	 *
1401
	 * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
1402
	 * the redirect.  It must be clearable by JavaScript code, so it must not be
1403
	 * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
1404
	 * variable.
1405
	 *
1406
	 * If the variable were set on the server, it would be cached, which is unwanted
1407
	 * since the post-edit state should only apply to the load right after the save.
1408
	 *
1409
	 * @param int $statusValue The status value (to check for new article status)
1410
	 */
1411
	protected function setPostEditCookie( $statusValue ) {
1412
		$revisionId = $this->page->getLatest();
1413
		$postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1414
1415
		$val = 'saved';
1416
		if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1417
			$val = 'created';
1418
		} elseif ( $this->oldid ) {
1419
			$val = 'restored';
1420
		}
1421
1422
		$response = $this->context->getRequest()->response();
1423
		$response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1424
			'httpOnly' => false,
1425
		] );
1426
	}
1427
1428
	/**
1429
	 * Attempt submission
1430
	 * @param array|bool $resultDetails See docs for $result in internalAttemptSave
1431
	 * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
1432
	 * @return Status The resulting status object.
1433
	 */
1434
	public function attemptSave( &$resultDetails = false ) {
1435
		# Allow bots to exempt some edits from bot flagging
1436
		$bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1437
		$status = $this->internalAttemptSave( $resultDetails, $bot );
0 ignored issues
show
Bug introduced by
It seems like $resultDetails defined by parameter $resultDetails on line 1434 can also be of type boolean; 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...
1438
1439
		Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1440
1441
		return $status;
1442
	}
1443
1444
	/**
1445
	 * Handle status, such as after attempt save
1446
	 *
1447
	 * @param Status $status
1448
	 * @param array|bool $resultDetails
1449
	 *
1450
	 * @throws ErrorPageError
1451
	 * @return bool False, if output is done, true if rest of the form should be displayed
1452
	 */
1453
	private function handleStatus( Status $status, $resultDetails ) {
1454
		/**
1455
		 * @todo FIXME: once the interface for internalAttemptSave() is made
1456
		 *   nicer, this should use the message in $status
1457
		 */
1458
		if ( $status->value == self::AS_SUCCESS_UPDATE
1459
			|| $status->value == self::AS_SUCCESS_NEW_ARTICLE
1460
		) {
1461
			$this->didSave = true;
1462
			if ( !$resultDetails['nullEdit'] ) {
1463
				$this->setPostEditCookie( $status->value );
1464
			}
1465
		}
1466
1467
		$out = $this->context->getOutput();
1468
1469
		// "wpExtraQueryRedirect" is a hidden input to modify
1470
		// after save URL and is not used by actual edit form
1471
		$request = $this->context->getRequest();
1472
		$extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1473
1474
		switch ( $status->value ) {
1475
			case self::AS_HOOK_ERROR_EXPECTED:
1476
			case self::AS_CONTENT_TOO_BIG:
1477
			case self::AS_ARTICLE_WAS_DELETED:
1478
			case self::AS_CONFLICT_DETECTED:
1479
			case self::AS_SUMMARY_NEEDED:
1480
			case self::AS_TEXTBOX_EMPTY:
1481
			case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1482
			case self::AS_END:
1483
			case self::AS_BLANK_ARTICLE:
1484
			case self::AS_SELF_REDIRECT:
1485
				return true;
1486
1487
			case self::AS_HOOK_ERROR:
1488
				return false;
1489
1490
			case self::AS_CANNOT_USE_CUSTOM_MODEL:
1491
			case self::AS_PARSE_ERROR:
1492
				$out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1493
				return true;
1494
1495
			case self::AS_SUCCESS_NEW_ARTICLE:
1496
				$query = $resultDetails['redirect'] ? 'redirect=no' : '';
1497
				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...
1498
					if ( $query === '' ) {
1499
						$query = $extraQueryRedirect;
1500
					} else {
1501
						$query = $query . '&' . $extraQueryRedirect;
1502
					}
1503
				}
1504
				$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1505
				$out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1506
				return false;
1507
1508
			case self::AS_SUCCESS_UPDATE:
1509
				$extraQuery = '';
1510
				$sectionanchor = $resultDetails['sectionanchor'];
1511
1512
				// Give extensions a chance to modify URL query on update
1513
				Hooks::run(
1514
					'ArticleUpdateBeforeRedirect',
1515
					[ $this->mArticle, &$sectionanchor, &$extraQuery ]
1516
				);
1517
1518
				if ( $resultDetails['redirect'] ) {
1519
					if ( $extraQuery == '' ) {
1520
						$extraQuery = 'redirect=no';
1521
					} else {
1522
						$extraQuery = 'redirect=no&' . $extraQuery;
1523
					}
1524
				}
1525
				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...
1526
					if ( $extraQuery === '' ) {
1527
						$extraQuery = $extraQueryRedirect;
1528
					} else {
1529
						$extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1530
					}
1531
				}
1532
1533
				$out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1534
				return false;
1535
1536
			case self::AS_SPAM_ERROR:
1537
				$this->spamPageWithContent( $resultDetails['spam'] );
1538
				return false;
1539
1540
			case self::AS_BLOCKED_PAGE_FOR_USER:
1541
				throw new UserBlockedError( $this->context->getUser()->getBlock() );
0 ignored issues
show
Bug introduced by
It seems like $this->context->getUser()->getBlock() can be null; however, __construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1542
1543
			case self::AS_IMAGE_REDIRECT_ANON:
1544
			case self::AS_IMAGE_REDIRECT_LOGGED:
1545
				throw new PermissionsError( 'upload' );
1546
1547
			case self::AS_READ_ONLY_PAGE_ANON:
1548
			case self::AS_READ_ONLY_PAGE_LOGGED:
1549
				throw new PermissionsError( 'edit' );
1550
1551
			case self::AS_READ_ONLY_PAGE:
1552
				throw new ReadOnlyError;
1553
1554
			case self::AS_RATE_LIMITED:
1555
				throw new ThrottledError();
1556
1557
			case self::AS_NO_CREATE_PERMISSION:
1558
				$permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1559
				throw new PermissionsError( $permission );
1560
1561
			case self::AS_NO_CHANGE_CONTENT_MODEL:
1562
				throw new PermissionsError( 'editcontentmodel' );
1563
1564
			default:
1565
				// We don't recognize $status->value. The only way that can happen
1566
				// is if an extension hook aborted from inside ArticleSave.
1567
				// Render the status object into $this->hookError
1568
				// FIXME this sucks, we should just use the Status object throughout
1569
				$this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1570
					'</div>';
1571
				return true;
1572
		}
1573
	}
1574
1575
	/**
1576
	 * Run hooks that can filter edits just before they get saved.
1577
	 *
1578
	 * @param Content $content The Content to filter.
1579
	 * @param Status $status For reporting the outcome to the caller
1580
	 * @param User $user The user performing the edit
1581
	 *
1582
	 * @return bool
1583
	 */
1584
	protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1585
		// Run old style post-section-merge edit filter
1586
		if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1587
			[ $this, $content, &$this->hookError, $this->summary ] )
1588
		) {
1589
			# Error messages etc. could be handled within the hook...
1590
			$status->fatal( 'hookaborted' );
1591
			$status->value = self::AS_HOOK_ERROR;
1592
			return false;
1593
		} elseif ( $this->hookError != '' ) {
1594
			# ...or the hook could be expecting us to produce an error
1595
			$status->fatal( 'hookaborted' );
1596
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1597
			return false;
1598
		}
1599
1600
		// Run new style post-section-merge edit filter
1601
		if ( !Hooks::run( 'EditFilterMergedContent',
1602
				[ $this->context, $content, $status, $this->summary,
1603
				$user, $this->minoredit ] )
1604
		) {
1605
			# Error messages etc. could be handled within the hook...
1606
			if ( $status->isGood() ) {
1607
				$status->fatal( 'hookaborted' );
1608
				// Not setting $this->hookError here is a hack to allow the hook
1609
				// to cause a return to the edit page without $this->hookError
1610
				// being set. This is used by ConfirmEdit to display a captcha
1611
				// without any error message cruft.
1612
			} else {
1613
				$this->hookError = $status->getWikiText();
1614
			}
1615
			// Use the existing $status->value if the hook set it
1616
			if ( !$status->value ) {
1617
				$status->value = self::AS_HOOK_ERROR;
1618
			}
1619
			return false;
1620
		} elseif ( !$status->isOK() ) {
1621
			# ...or the hook could be expecting us to produce an error
1622
			// FIXME this sucks, we should just use the Status object throughout
1623
			$this->hookError = $status->getWikiText();
1624
			$status->fatal( 'hookaborted' );
1625
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1626
			return false;
1627
		}
1628
1629
		return true;
1630
	}
1631
1632
	/**
1633
	 * Return the summary to be used for a new section.
1634
	 *
1635
	 * @param string $sectionanchor Set to the section anchor text
1636
	 * @return string
1637
	 */
1638
	private function newSectionSummary( &$sectionanchor = null ) {
1639
		global $wgParser;
1640
1641
		if ( $this->sectiontitle !== '' ) {
1642
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1643
			// If no edit summary was specified, create one automatically from the section
1644
			// title and have it link to the new section. Otherwise, respect the summary as
1645
			// passed.
1646
			if ( $this->summary === '' ) {
1647
				$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1648
				return wfMessage( 'newsectionsummary' )
1649
					->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1650
			}
1651
		} elseif ( $this->summary !== '' ) {
1652
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1653
			# This is a new section, so create a link to the new section
1654
			# in the revision summary.
1655
			$cleanSummary = $wgParser->stripSectionName( $this->summary );
1656
			return wfMessage( 'newsectionsummary' )
1657
				->rawParams( $cleanSummary )->inContentLanguage()->text();
1658
		}
1659
		return $this->summary;
1660
	}
1661
1662
	/**
1663
	 * Attempt submission (no UI)
1664
	 *
1665
	 * @param array $result Array to add statuses to, currently with the
1666
	 *   possible keys:
1667
	 *   - spam (string): Spam string from content if any spam is detected by
1668
	 *     matchSpamRegex.
1669
	 *   - sectionanchor (string): Section anchor for a section save.
1670
	 *   - nullEdit (boolean): Set if doEditContent is OK.  True if null edit,
1671
	 *     false otherwise.
1672
	 *   - redirect (bool): Set if doEditContent is OK. True if resulting
1673
	 *     revision is a redirect.
1674
	 * @param bool $bot True if edit is being made under the bot right.
1675
	 *
1676
	 * @return Status Status object, possibly with a message, but always with
1677
	 *   one of the AS_* constants in $status->value,
1678
	 *
1679
	 * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to
1680
	 *   various error display idiosyncrasies. There are also lots of cases
1681
	 *   where error metadata is set in the object and retrieved later instead
1682
	 *   of being returned, e.g. AS_CONTENT_TOO_BIG and
1683
	 *   AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some
1684
	 * time.
1685
	 */
1686
	function internalAttemptSave( &$result, $bot = false ) {
1687
		global $wgParser, $wgMaxArticleSize, $wgContentHandlerUseDB;
1688
1689
		$status = Status::newGood();
1690
		$user = $this->context->getUser();
1691
		$request = $this->context->getRequest();
1692
1693
		if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1694
			wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1695
			$status->fatal( 'hookaborted' );
1696
			$status->value = self::AS_HOOK_ERROR;
1697
			return $status;
1698
		}
1699
1700
		$spam = $request->getText( 'wpAntispam' );
1701
		if ( $spam !== '' ) {
1702
			wfDebugLog(
1703
				'SimpleAntiSpam',
1704
				$user->getName() .
1705
				' editing "' .
1706
				$this->mTitle->getPrefixedText() .
1707
				'" submitted bogus field "' .
1708
				$spam .
1709
				'"'
1710
			);
1711
			$status->fatal( 'spamprotectionmatch', false );
1712
			$status->value = self::AS_SPAM_ERROR;
1713
			return $status;
1714
		}
1715
1716
		try {
1717
			# Construct Content object
1718
			$textbox_content = $this->toEditContent( $this->textbox1 );
1719
		} catch ( MWContentSerializationException $ex ) {
1720
			$status->fatal(
1721
				'content-failed-to-parse',
1722
				$this->contentModel,
1723
				$this->contentFormat,
1724
				$ex->getMessage()
1725
			);
1726
			$status->value = self::AS_PARSE_ERROR;
1727
			return $status;
1728
		}
1729
1730
		# Check image redirect
1731
		if ( $this->mTitle->getNamespace() == NS_FILE &&
1732
			$textbox_content->isRedirect() &&
1733
			!$user->isAllowed( 'upload' )
1734
		) {
1735
				$code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1736
				$status->setResult( false, $code );
1737
1738
				return $status;
1739
		}
1740
1741
		# Check for spam
1742
		$match = self::matchSummarySpamRegex( $this->summary );
1743
		if ( $match === false && $this->section == 'new' ) {
1744
			# $wgSpamRegex is enforced on this new heading/summary because, unlike
1745
			# regular summaries, it is added to the actual wikitext.
1746
			if ( $this->sectiontitle !== '' ) {
1747
				# This branch is taken when the API is used with the 'sectiontitle' parameter.
1748
				$match = self::matchSpamRegex( $this->sectiontitle );
1749
			} else {
1750
				# This branch is taken when the "Add Topic" user interface is used, or the API
1751
				# is used with the 'summary' parameter.
1752
				$match = self::matchSpamRegex( $this->summary );
1753
			}
1754
		}
1755
		if ( $match === false ) {
1756
			$match = self::matchSpamRegex( $this->textbox1 );
1757
		}
1758
		if ( $match !== false ) {
1759
			$result['spam'] = $match;
1760
			$ip = $request->getIP();
1761
			$pdbk = $this->mTitle->getPrefixedDBkey();
1762
			$match = str_replace( "\n", '', $match );
1763
			wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1764
			$status->fatal( 'spamprotectionmatch', $match );
1765
			$status->value = self::AS_SPAM_ERROR;
1766
			return $status;
1767
		}
1768
		if ( !Hooks::run(
1769
			'EditFilter',
1770
			[ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1771
		) {
1772
			# Error messages etc. could be handled within the hook...
1773
			$status->fatal( 'hookaborted' );
1774
			$status->value = self::AS_HOOK_ERROR;
1775
			return $status;
1776
		} elseif ( $this->hookError != '' ) {
1777
			# ...or the hook could be expecting us to produce an error
1778
			$status->fatal( 'hookaborted' );
1779
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1780
			return $status;
1781
		}
1782
1783
		if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
1784
			// Auto-block user's IP if the account was "hard" blocked
1785
			if ( !wfReadOnly() ) {
1786
				$user->spreadAnyEditBlock();
1787
			}
1788
			# Check block state against master, thus 'false'.
1789
			$status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1790
			return $status;
1791
		}
1792
1793
		$this->contentLength = strlen( $this->textbox1 );
1794 View Code Duplication
		if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
1795
			// Error will be displayed by showEditForm()
1796
			$this->tooBig = true;
1797
			$status->setResult( false, self::AS_CONTENT_TOO_BIG );
1798
			return $status;
1799
		}
1800
1801 View Code Duplication
		if ( !$user->isAllowed( 'edit' ) ) {
1802
			if ( $user->isAnon() ) {
1803
				$status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1804
				return $status;
1805
			} else {
1806
				$status->fatal( 'readonlytext' );
1807
				$status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1808
				return $status;
1809
			}
1810
		}
1811
1812
		$changingContentModel = false;
1813
		if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1814 View Code Duplication
			if ( !$wgContentHandlerUseDB ) {
1815
				$status->fatal( 'editpage-cannot-use-custom-model' );
1816
				$status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1817
				return $status;
1818
			} elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
1819
				$status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1820
				return $status;
1821
1822
			}
1823
			$changingContentModel = true;
1824
			$oldContentModel = $this->mTitle->getContentModel();
1825
		}
1826
1827
		if ( $this->changeTags ) {
1828
			$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1829
				$this->changeTags, $user );
1830
			if ( !$changeTagsStatus->isOK() ) {
1831
				$changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1832
				return $changeTagsStatus;
1833
			}
1834
		}
1835
1836
		if ( wfReadOnly() ) {
1837
			$status->fatal( 'readonlytext' );
1838
			$status->value = self::AS_READ_ONLY_PAGE;
1839
			return $status;
1840
		}
1841
		if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) ) {
1842
			$status->fatal( 'actionthrottledtext' );
1843
			$status->value = self::AS_RATE_LIMITED;
1844
			return $status;
1845
		}
1846
1847
		# If the article has been deleted while editing, don't save it without
1848
		# confirmation
1849
		if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1850
			$status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1851
			return $status;
1852
		}
1853
1854
		# Load the page data from the master. If anything changes in the meantime,
1855
		# we detect it by using page_latest like a token in a 1 try compare-and-swap.
1856
		$this->page->loadPageData( 'fromdbmaster' );
1857
		$new = !$this->page->exists();
1858
1859
		if ( $new ) {
1860
			// Late check for create permission, just in case *PARANOIA*
1861
			if ( !$this->mTitle->userCan( 'create', $user ) ) {
1862
				$status->fatal( 'nocreatetext' );
1863
				$status->value = self::AS_NO_CREATE_PERMISSION;
1864
				wfDebug( __METHOD__ . ": no create permission\n" );
1865
				return $status;
1866
			}
1867
1868
			// Don't save a new page if it's blank or if it's a MediaWiki:
1869
			// message with content equivalent to default (allow empty pages
1870
			// in this case to disable messages, see bug 50124)
1871
			$defaultMessageText = $this->mTitle->getDefaultMessageText();
1872
			if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1873
				$defaultText = $defaultMessageText;
1874
			} else {
1875
				$defaultText = '';
1876
			}
1877
1878
			if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1879
				$this->blankArticle = true;
1880
				$status->fatal( 'blankarticle' );
1881
				$status->setResult( false, self::AS_BLANK_ARTICLE );
1882
				return $status;
1883
			}
1884
1885
			if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1718 can also be of type false or null; 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...
1886
				return $status;
1887
			}
1888
1889
			$content = $textbox_content;
1890
1891
			$result['sectionanchor'] = '';
1892
			if ( $this->section == 'new' ) {
1893
				if ( $this->sectiontitle !== '' ) {
1894
					// Insert the section title above the content.
1895
					$content = $content->addSectionHeader( $this->sectiontitle );
1896
				} elseif ( $this->summary !== '' ) {
1897
					// Insert the section title above the content.
1898
					$content = $content->addSectionHeader( $this->summary );
1899
				}
1900
				$this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1901
			}
1902
1903
			$status->value = self::AS_SUCCESS_NEW_ARTICLE;
1904
1905
		} else { # not $new
1906
1907
			# Article exists. Check for edit conflict.
1908
1909
			$this->page->clear(); # Force reload of dates, etc.
1910
			$timestamp = $this->page->getTimestamp();
1911
			$latest = $this->page->getLatest();
1912
1913
			wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1914
1915
			// Check editRevId if set, which handles same-second timestamp collisions
1916
			if ( $timestamp != $this->edittime
1917
				|| ( $this->editRevId !== null && $this->editRevId != $latest )
1918
			) {
1919
				$this->isConflict = true;
1920
				if ( $this->section == 'new' ) {
1921
					if ( $this->page->getUserText() == $user->getName() &&
1922
						$this->page->getComment() == $this->newSectionSummary()
1923
					) {
1924
						// Probably a duplicate submission of a new comment.
1925
						// This can happen when CDN resends a request after
1926
						// a timeout but the first one actually went through.
1927
						wfDebug( __METHOD__
1928
							. ": duplicate new section submission; trigger edit conflict!\n" );
1929
					} else {
1930
						// New comment; suppress conflict.
1931
						$this->isConflict = false;
1932
						wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1933
					}
1934
				} elseif ( $this->section == ''
1935
					&& Revision::userWasLastToEdit(
1936
						DB_MASTER, $this->mTitle->getArticleID(),
1937
						$user->getId(), $this->edittime
1938
					)
1939
				) {
1940
					# Suppress edit conflict with self, except for section edits where merging is required.
1941
					wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1942
					$this->isConflict = false;
1943
				}
1944
			}
1945
1946
			// If sectiontitle is set, use it, otherwise use the summary as the section title.
1947
			if ( $this->sectiontitle !== '' ) {
1948
				$sectionTitle = $this->sectiontitle;
1949
			} else {
1950
				$sectionTitle = $this->summary;
1951
			}
1952
1953
			$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...
1954
1955
			if ( $this->isConflict ) {
1956
				wfDebug( __METHOD__
1957
					. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1958
					. " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
1959
				// @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
1960
				// ...or disable section editing for non-current revisions (not exposed anyway).
1961
				if ( $this->editRevId !== null ) {
1962
					$content = $this->page->replaceSectionAtRev(
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 1718 can also be of type false or null; however, WikiPage::replaceSectionAtRev() 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...
1965
						$sectionTitle,
1966
						$this->editRevId
1967
					);
1968
				} else {
1969
					$content = $this->page->replaceSectionContent(
1970
						$this->section,
1971
						$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1718 can also be of type false or null; however, WikiPage::replaceSectionContent() 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...
1972
						$sectionTitle,
1973
						$this->edittime
1974
					);
1975
				}
1976
			} else {
1977
				wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1978
				$content = $this->page->replaceSectionContent(
1979
					$this->section,
1980
					$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1718 can also be of type false or null; however, WikiPage::replaceSectionContent() 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...
1981
					$sectionTitle
1982
				);
1983
			}
1984
1985
			if ( is_null( $content ) ) {
1986
				wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1987
				$this->isConflict = true;
1988
				$content = $textbox_content; // do not try to merge here!
1989
			} elseif ( $this->isConflict ) {
1990
				# Attempt merge
1991
				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...
1992
					// Successful merge! Maybe we should tell the user the good news?
1993
					$this->isConflict = false;
1994
					wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1995
				} else {
1996
					$this->section = '';
1997
					$this->textbox1 = ContentHandler::getContentText( $content );
1998
					wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1999
				}
2000
			}
2001
2002
			if ( $this->isConflict ) {
2003
				$status->setResult( false, self::AS_CONFLICT_DETECTED );
2004
				return $status;
2005
			}
2006
2007
			if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2008
				return $status;
2009
			}
2010
2011
			if ( $this->section == 'new' ) {
2012
				// Handle the user preference to force summaries here
2013
				if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2014
					$this->missingSummary = true;
2015
					$status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2016
					$status->value = self::AS_SUMMARY_NEEDED;
2017
					return $status;
2018
				}
2019
2020
				// Do not allow the user to post an empty comment
2021
				if ( $this->textbox1 == '' ) {
2022
					$this->missingComment = true;
2023
					$status->fatal( 'missingcommenttext' );
2024
					$status->value = self::AS_TEXTBOX_EMPTY;
2025
					return $status;
2026
				}
2027
			} elseif ( !$this->allowBlankSummary
2028
				&& !$content->equals( $this->getOriginalContent( $user ) )
2029
				&& !$content->isRedirect()
2030
				&& md5( $this->summary ) == $this->autoSumm
2031
			) {
2032
				$this->missingSummary = true;
2033
				$status->fatal( 'missingsummary' );
2034
				$status->value = self::AS_SUMMARY_NEEDED;
2035
				return $status;
2036
			}
2037
2038
			# All's well
2039
			$sectionanchor = '';
2040
			if ( $this->section == 'new' ) {
2041
				$this->summary = $this->newSectionSummary( $sectionanchor );
2042
			} elseif ( $this->section != '' ) {
2043
				# Try to get a section anchor from the section source, redirect
2044
				# to edited section if header found.
2045
				# XXX: Might be better to integrate this into Article::replaceSectionAtRev
2046
				# for duplicate heading checking and maybe parsing.
2047
				$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2048
				# We can't deal with anchors, includes, html etc in the header for now,
2049
				# headline would need to be parsed to improve this.
2050
				if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2051
					$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
2052
				}
2053
			}
2054
			$result['sectionanchor'] = $sectionanchor;
2055
2056
			// Save errors may fall down to the edit form, but we've now
2057
			// merged the section into full text. Clear the section field
2058
			// so that later submission of conflict forms won't try to
2059
			// replace that into a duplicated mess.
2060
			$this->textbox1 = $this->toEditText( $content );
2061
			$this->section = '';
2062
2063
			$status->value = self::AS_SUCCESS_UPDATE;
2064
		}
2065
2066
		if ( !$this->allowSelfRedirect
2067
			&& $content->isRedirect()
2068
			&& $content->getRedirectTarget()->equals( $this->getTitle() )
2069
		) {
2070
			// If the page already redirects to itself, don't warn.
2071
			$currentTarget = $this->getCurrentContent()->getRedirectTarget();
2072
			if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2073
				$this->selfRedirect = true;
2074
				$status->fatal( 'selfredirect' );
2075
				$status->value = self::AS_SELF_REDIRECT;
2076
				return $status;
2077
			}
2078
		}
2079
2080
		// Check for length errors again now that the section is merged in
2081
		$this->contentLength = strlen( $this->toEditText( $content ) );
2082 View Code Duplication
		if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
2083
			$this->tooBig = true;
2084
			$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2085
			return $status;
2086
		}
2087
2088
		$flags = EDIT_AUTOSUMMARY |
2089
			( $new ? EDIT_NEW : EDIT_UPDATE ) |
2090
			( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2091
			( $bot ? EDIT_FORCE_BOT : 0 );
2092
2093
		$doEditStatus = $this->page->doEditContent(
2094
			$content,
2095
			$this->summary,
2096
			$flags,
2097
			false,
2098
			$user,
2099
			$content->getDefaultFormat(),
2100
			$this->changeTags
2101
		);
2102
2103
		if ( !$doEditStatus->isOK() ) {
2104
			// Failure from doEdit()
2105
			// Show the edit conflict page for certain recognized errors from doEdit(),
2106
			// but don't show it for errors from extension hooks
2107
			$errors = $doEditStatus->getErrorsArray();
2108
			if ( in_array( $errors[0][0],
2109
					[ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2110
			) {
2111
				$this->isConflict = true;
2112
				// Destroys data doEdit() put in $status->value but who cares
2113
				$doEditStatus->value = self::AS_END;
2114
			}
2115
			return $doEditStatus;
2116
		}
2117
2118
		$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2119
		if ( $result['nullEdit'] ) {
2120
			// We don't know if it was a null edit until now, so increment here
2121
			$user->pingLimiter( 'linkpurge' );
2122
		}
2123
		$result['redirect'] = $content->isRedirect();
2124
2125
		$this->updateWatchlist();
2126
2127
		// If the content model changed, add a log entry
2128
		if ( $changingContentModel ) {
2129
			$this->addContentModelChangeLogEntry(
2130
				$user,
2131
				$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...
2132
				$this->contentModel,
2133
				$this->summary
2134
			);
2135
		}
2136
2137
		return $status;
2138
	}
2139
2140
	/**
2141
	 * @param User $user
2142
	 * @param string|false $oldModel false if the page is being newly created
2143
	 * @param string $newModel
2144
	 * @param string $reason
2145
	 */
2146
	protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2147
		$new = $oldModel === false;
2148
		$log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2149
		$log->setPerformer( $user );
2150
		$log->setTarget( $this->mTitle );
2151
		$log->setComment( $reason );
2152
		$log->setParameters( [
2153
			'4::oldmodel' => $oldModel,
2154
			'5::newmodel' => $newModel
2155
		] );
2156
		$logid = $log->insert();
2157
		$log->publish( $logid );
2158
	}
2159
2160
	/**
2161
	 * Register the change of watch status
2162
	 */
2163
	protected function updateWatchlist() {
2164
		$user = $this->context->getUser();
2165
2166
		if ( !$user->isLoggedIn() ) {
2167
			return;
2168
		}
2169
2170
		$title = $this->mTitle;
2171
		$watch = $this->watchthis;
2172
		// Do this in its own transaction to reduce contention...
2173
		DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2174
			if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2175
				return; // nothing to change
2176
			}
2177
			WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2178
		} );
2179
	}
2180
2181
	/**
2182
	 * Attempts to do 3-way merge of edit content with a base revision
2183
	 * and current content, in case of edit conflict, in whichever way appropriate
2184
	 * for the content type.
2185
	 *
2186
	 * @since 1.21
2187
	 *
2188
	 * @param Content $editContent
2189
	 *
2190
	 * @return bool
2191
	 */
2192
	private function mergeChangesIntoContent( &$editContent ) {
2193
2194
		$db = wfGetDB( DB_MASTER );
2195
2196
		// This is the revision the editor started from
2197
		$baseRevision = $this->getBaseRevision();
2198
		$baseContent = $baseRevision ? $baseRevision->getContent() : null;
2199
2200
		if ( is_null( $baseContent ) ) {
2201
			return false;
2202
		}
2203
2204
		// The current state, we want to merge updates into it
2205
		$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2206
		$currentContent = $currentRevision ? $currentRevision->getContent() : null;
2207
2208
		if ( is_null( $currentContent ) ) {
2209
			return false;
2210
		}
2211
2212
		$handler = ContentHandler::getForModelID( $baseContent->getModel() );
2213
2214
		$result = $handler->merge3( $baseContent, $editContent, $currentContent );
2215
2216
		if ( $result ) {
2217
			$editContent = $result;
2218
			// Update parentRevId to what we just merged.
2219
			$this->parentRevId = $currentRevision->getId();
2220
			return true;
2221
		}
2222
2223
		return false;
2224
	}
2225
2226
	/**
2227
	 * @note: this method is very poorly named. If the user opened the form with ?oldid=X,
2228
	 *        one might think of X as the "base revision", which is NOT what this returns.
2229
	 * @return Revision Current version when the edit was started
2230
	 */
2231
	function getBaseRevision() {
2232
		if ( !$this->mBaseRevision ) {
2233
			$db = wfGetDB( DB_MASTER );
2234
			$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...
2235
				? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2236
				: Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2237
		}
2238
		return $this->mBaseRevision;
2239
	}
2240
2241
	/**
2242
	 * Check given input text against $wgSpamRegex, and return the text of the first match.
2243
	 *
2244
	 * @param string $text
2245
	 *
2246
	 * @return string|bool Matching string or false
2247
	 */
2248
	public static function matchSpamRegex( $text ) {
2249
		global $wgSpamRegex;
2250
		// For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2251
		$regexes = (array)$wgSpamRegex;
2252
		return self::matchSpamRegexInternal( $text, $regexes );
2253
	}
2254
2255
	/**
2256
	 * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
2257
	 *
2258
	 * @param string $text
2259
	 *
2260
	 * @return string|bool Matching string or false
2261
	 */
2262
	public static function matchSummarySpamRegex( $text ) {
2263
		global $wgSummarySpamRegex;
2264
		$regexes = (array)$wgSummarySpamRegex;
2265
		return self::matchSpamRegexInternal( $text, $regexes );
2266
	}
2267
2268
	/**
2269
	 * @param string $text
2270
	 * @param array $regexes
2271
	 * @return bool|string
2272
	 */
2273
	protected static function matchSpamRegexInternal( $text, $regexes ) {
2274
		foreach ( $regexes as $regex ) {
2275
			$matches = [];
2276
			if ( preg_match( $regex, $text, $matches ) ) {
2277
				return $matches[0];
2278
			}
2279
		}
2280
		return false;
2281
	}
2282
2283
	function setHeaders() {
2284
		global $wgAjaxEditStash;
2285
2286
		$out = $this->context->getOutput();
2287
		$user = $this->context->getUser();
2288
2289
		$out->addModules( 'mediawiki.action.edit' );
2290
		$out->addModuleStyles( 'mediawiki.action.edit.styles' );
2291
2292
		if ( $user->getOption( 'showtoolbar' ) ) {
2293
			// The addition of default buttons is handled by getEditToolbar() which
2294
			// has its own dependency on this module. The call here ensures the module
2295
			// is loaded in time (it has position "top") for other modules to register
2296
			// buttons (e.g. extensions, gadgets, user scripts).
2297
			$out->addModules( 'mediawiki.toolbar' );
2298
		}
2299
2300
		if ( $user->getOption( 'uselivepreview' ) ) {
2301
			$out->addModules( 'mediawiki.action.edit.preview' );
2302
		}
2303
2304
		if ( $user->getOption( 'useeditwarning' ) ) {
2305
			$out->addModules( 'mediawiki.action.edit.editWarning' );
2306
		}
2307
2308
		# Enabled article-related sidebar, toplinks, etc.
2309
		$out->setArticleRelated( true );
2310
2311
		$contextTitle = $this->getContextTitle();
2312
		if ( $this->isConflict ) {
2313
			$msg = 'editconflict';
2314
		} elseif ( $contextTitle->exists() && $this->section != '' ) {
2315
			$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2316
		} else {
2317
			$msg = $contextTitle->exists()
2318
				|| ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2319
					&& $contextTitle->getDefaultMessageText() !== false
2320
				)
2321
				? 'editing'
2322
				: 'creating';
2323
		}
2324
2325
		# Use the title defined by DISPLAYTITLE magic word when present
2326
		# NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2327
		#       setPageTitle() treats the input as wikitext, which should be safe in either case.
2328
		$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2329
		if ( $displayTitle === false ) {
2330
			$displayTitle = $contextTitle->getPrefixedText();
2331
		}
2332
		$out->setPageTitle( wfMessage( $msg, $displayTitle ) );
2333
		# Transmit the name of the message to JavaScript for live preview
2334
		# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2335
		$out->addJsConfigVars( [
2336
			'wgEditMessage' => $msg,
2337
			'wgAjaxEditStash' => $wgAjaxEditStash,
2338
		] );
2339
	}
2340
2341
	/**
2342
	 * Show all applicable editing introductions
2343
	 */
2344
	protected function showIntro() {
2345
		if ( $this->suppressIntro ) {
2346
			return;
2347
		}
2348
2349
		$out = $this->context->getOutput();
2350
		$namespace = $this->mTitle->getNamespace();
2351
2352
		if ( $namespace == NS_MEDIAWIKI ) {
2353
			# Show a warning if editing an interface message
2354
			$out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2355
			# If this is a default message (but not css or js),
2356
			# show a hint that it is translatable on translatewiki.net
2357
			if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2358
				&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2359
			) {
2360
				$defaultMessageText = $this->mTitle->getDefaultMessageText();
2361
				if ( $defaultMessageText !== false ) {
2362
					$out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2363
						'translateinterface' );
2364
				}
2365
			}
2366
		} elseif ( $namespace == NS_FILE ) {
2367
			# Show a hint to shared repo
2368
			$file = wfFindFile( $this->mTitle );
2369
			if ( $file && !$file->isLocal() ) {
2370
				$descUrl = $file->getDescriptionUrl();
2371
				# there must be a description url to show a hint to shared repo
2372
				if ( $descUrl ) {
2373
					if ( !$this->mTitle->exists() ) {
2374
						$out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2375
									'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2376
						] );
2377
					} else {
2378
						$out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2379
									'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2380
						] );
2381
					}
2382
				}
2383
			}
2384
		}
2385
2386
		# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2387
		# Show log extract when the user is currently blocked
2388
		if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2389
			$username = explode( '/', $this->mTitle->getText(), 2 )[0];
2390
			$user = User::newFromName( $username, false /* allow IP users*/ );
2391
			$ip = User::isIP( $username );
2392
			$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 2390 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 2390 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...
2393
			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2394
				$out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2395
					[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2396 View Code Duplication
			} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2397
				# Show log extract if the user is currently blocked
2398
				LogEventsList::showLogExtract(
2399
					$out,
2400
					'block',
2401
					MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2402
					'',
2403
					[
2404
						'lim' => 1,
2405
						'showIfEmpty' => false,
2406
						'msgKey' => [
2407
							'blocked-notice-logextract',
2408
							$user->getName() # Support GENDER in notice
2409
						]
2410
					]
2411
				);
2412
			}
2413
		}
2414
		# Try to add a custom edit intro, or use the standard one if this is not possible.
2415
		if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2416
			$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2417
				wfMessage( 'helppage' )->inContentLanguage()->text()
2418
			) );
2419
			if ( $this->context->getUser()->isLoggedIn() ) {
2420
				$out->wrapWikiMsg(
2421
					// Suppress the external link icon, consider the help url an internal one
2422
					"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2423
					[
2424
						'newarticletext',
2425
						$helpLink
2426
					]
2427
				);
2428
			} else {
2429
				$out->wrapWikiMsg(
2430
					// Suppress the external link icon, consider the help url an internal one
2431
					"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2432
					[
2433
						'newarticletextanon',
2434
						$helpLink
2435
					]
2436
				);
2437
			}
2438
		}
2439
		# Give a notice if the user is editing a deleted/moved page...
2440 View Code Duplication
		if ( !$this->mTitle->exists() ) {
2441
			LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2442
				'',
2443
				[
2444
					'lim' => 10,
2445
					'conds' => [ "log_action != 'revision'" ],
2446
					'showIfEmpty' => false,
2447
					'msgKey' => [ 'recreate-moveddeleted-warn' ]
2448
				]
2449
			);
2450
		}
2451
	}
2452
2453
	/**
2454
	 * Attempt to show a custom editing introduction, if supplied
2455
	 *
2456
	 * @return bool
2457
	 */
2458
	protected function showCustomIntro() {
2459
		if ( $this->editintro ) {
2460
			$title = Title::newFromText( $this->editintro );
2461
			if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2462
				// Added using template syntax, to take <noinclude>'s into account.
2463
				$this->context->getOutput()->addWikiTextTitleTidy(
2464
					'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2465
					$this->mTitle
2466
				);
2467
				return true;
2468
			}
2469
		}
2470
		return false;
2471
	}
2472
2473
	/**
2474
	 * Gets an editable textual representation of $content.
2475
	 * The textual representation can be turned by into a Content object by the
2476
	 * toEditContent() method.
2477
	 *
2478
	 * If $content is null or false or a string, $content is returned unchanged.
2479
	 *
2480
	 * If the given Content object is not of a type that can be edited using
2481
	 * the text base EditPage, an exception will be raised. Set
2482
	 * $this->allowNonTextContent to true to allow editing of non-textual
2483
	 * content.
2484
	 *
2485
	 * @param Content|null|bool|string $content
2486
	 * @return string The editable text form of the content.
2487
	 *
2488
	 * @throws MWException If $content is not an instance of TextContent and
2489
	 *   $this->allowNonTextContent is not true.
2490
	 */
2491
	protected function toEditText( $content ) {
2492
		if ( $content === null || $content === false || is_string( $content ) ) {
2493
			return $content;
2494
		}
2495
2496
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2497
			throw new MWException( 'This content model is not supported: '
2498
				. 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...
2499
		}
2500
2501
		return $content->serialize( $this->contentFormat );
2502
	}
2503
2504
	/**
2505
	 * Turns the given text into a Content object by unserializing it.
2506
	 *
2507
	 * If the resulting Content object is not of a type that can be edited using
2508
	 * the text base EditPage, an exception will be raised. Set
2509
	 * $this->allowNonTextContent to true to allow editing of non-textual
2510
	 * content.
2511
	 *
2512
	 * @param string|null|bool $text Text to unserialize
2513
	 * @return Content|bool|null The content object created from $text. If $text was false
2514
	 *   or null, false resp. null will be  returned instead.
2515
	 *
2516
	 * @throws MWException If unserializing the text results in a Content
2517
	 *   object that is not an instance of TextContent and
2518
	 *   $this->allowNonTextContent is not true.
2519
	 */
2520
	protected function toEditContent( $text ) {
2521
		if ( $text === false || $text === null ) {
2522
			return $text;
2523
		}
2524
2525
		$content = ContentHandler::makeContent( $text, $this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $text defined by parameter $text on line 2520 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...
2526
			$this->contentModel, $this->contentFormat );
2527
2528
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2529
			throw new MWException( 'This content model is not supported: '
2530
				. ContentHandler::getLocalizedName( $content->getModel() ) );
2531
		}
2532
2533
		return $content;
2534
	}
2535
2536
	/**
2537
	 * Send the edit form and related headers to $wgOut
2538
	 * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2539
	 *     during form output near the top, for captchas and the like.
2540
	 *
2541
	 * The $formCallback parameter is deprecated since MediaWiki 1.25. Please
2542
	 * use the EditPage::showEditForm:fields hook instead.
2543
	 */
2544
	function showEditForm( $formCallback = null ) {
2545
		# need to parse the preview early so that we know which templates are used,
2546
		# otherwise users with "show preview after edit box" will get a blank list
2547
		# we parse this near the beginning so that setHeaders can do the title
2548
		# setting work instead of leaving it in getPreviewText
2549
		$previewOutput = '';
2550
		if ( $this->formtype == 'preview' ) {
2551
			$previewOutput = $this->getPreviewText();
2552
		}
2553
2554
		$out = $this->context->getOutput();
2555
		Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$out ] );
2556
2557
		$this->setHeaders();
2558
2559
		if ( $this->showHeader() === false ) {
2560
			return;
2561
		}
2562
2563
		$out->addHTML( $this->editFormPageTop );
2564
2565
		$user = $this->context->getUser();
2566
		if ( $user->getOption( 'previewontop' ) ) {
2567
			$this->displayPreviewArea( $previewOutput, true );
2568
		}
2569
2570
		$out->addHTML( $this->editFormTextTop );
2571
2572
		$showToolbar = true;
2573
		if ( $this->wasDeletedSinceLastEdit() ) {
2574
			if ( $this->formtype == 'save' ) {
2575
				// Hide the toolbar and edit area, user can click preview to get it back
2576
				// Add an confirmation checkbox and explanation.
2577
				$showToolbar = false;
2578
			} else {
2579
				$out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2580
					'deletedwhileediting' );
2581
			}
2582
		}
2583
2584
		// @todo add EditForm plugin interface and use it here!
2585
		//       search for textarea1 and textares2, and allow EditForm to override all uses.
2586
		$out->addHTML( Html::openElement(
2587
			'form',
2588
			[
2589
				'id' => self::EDITFORM_ID,
2590
				'name' => self::EDITFORM_ID,
2591
				'method' => 'post',
2592
				'action' => $this->getActionURL( $this->getContextTitle() ),
2593
				'enctype' => 'multipart/form-data'
2594
			]
2595
		) );
2596
2597
		if ( is_callable( $formCallback ) ) {
2598
			wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2599
			call_user_func_array( $formCallback, [ &$out ] );
2600
		}
2601
2602
		// Add an empty field to trip up spambots
2603
		$out->addHTML(
2604
			Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2605
			. Html::rawElement(
2606
				'label',
2607
				[ 'for' => 'wpAntispam' ],
2608
				wfMessage( 'simpleantispam-label' )->parse()
2609
			)
2610
			. Xml::element(
2611
				'input',
2612
				[
2613
					'type' => 'text',
2614
					'name' => 'wpAntispam',
2615
					'id' => 'wpAntispam',
2616
					'value' => ''
2617
				]
2618
			)
2619
			. Xml::closeElement( 'div' )
2620
		);
2621
2622
		Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$out ] );
2623
2624
		// Put these up at the top to ensure they aren't lost on early form submission
2625
		$this->showFormBeforeText();
2626
2627
		if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2628
			$username = $this->lastDelete->user_name;
2629
			$comment = $this->lastDelete->log_comment;
2630
2631
			// It is better to not parse the comment at all than to have templates expanded in the middle
2632
			// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2633
			$key = $comment === ''
2634
				? 'confirmrecreate-noreason'
2635
				: 'confirmrecreate';
2636
			$out->addHTML(
2637
				'<div class="mw-confirm-recreate">' .
2638
					wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2639
				Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2640
					[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2641
				) .
2642
				'</div>'
2643
			);
2644
		}
2645
2646
		# When the summary is hidden, also hide them on preview/show changes
2647
		if ( $this->nosummary ) {
2648
			$out->addHTML( Html::hidden( 'nosummary', true ) );
2649
		}
2650
2651
		# If a blank edit summary was previously provided, and the appropriate
2652
		# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2653
		# user being bounced back more than once in the event that a summary
2654
		# is not required.
2655
		# ####
2656
		# For a bit more sophisticated detection of blank summaries, hash the
2657
		# automatic one and pass that in the hidden field wpAutoSummary.
2658
		if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2659
			$out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2660
		}
2661
2662
		if ( $this->undidRev ) {
2663
			$out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2664
		}
2665
2666
		if ( $this->selfRedirect ) {
2667
			$out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2668
		}
2669
2670
		if ( $this->hasPresetSummary ) {
2671
			// If a summary has been preset using &summary= we don't want to prompt for
2672
			// a different summary. Only prompt for a summary if the summary is blanked.
2673
			// (Bug 17416)
2674
			$this->autoSumm = md5( '' );
2675
		}
2676
2677
		$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2678
		$out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2679
2680
		$out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2681
		$out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2682
2683
		$out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2684
		$out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2685
2686 View Code Duplication
		if ( $this->section == 'new' ) {
2687
			$this->showSummaryInput( true, $this->summary );
2688
			$out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2689
		}
2690
2691
		$out->addHTML( $this->editFormTextBeforeContent );
2692
2693
		if ( !$this->isCssJsSubpage && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
2694
			$out->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2695
		}
2696
2697
		if ( $this->blankArticle ) {
2698
			$out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2699
		}
2700
2701
		if ( $this->isConflict ) {
2702
			// In an edit conflict bypass the overridable content form method
2703
			// and fallback to the raw wpTextbox1 since editconflicts can't be
2704
			// resolved between page source edits and custom ui edits using the
2705
			// custom edit ui.
2706
			$this->textbox2 = $this->textbox1;
2707
2708
			$content = $this->getCurrentContent();
2709
			$this->textbox1 = $this->toEditText( $content );
2710
2711
			$this->showTextbox1();
2712
		} else {
2713
			$this->showContentForm();
2714
		}
2715
2716
		$out->addHTML( $this->editFormTextAfterContent );
2717
2718
		$this->showStandardInputs();
2719
2720
		$this->showFormAfterText();
2721
2722
		$this->showTosSummary();
2723
2724
		$this->showEditTools();
2725
2726
		$out->addHTML( $this->editFormTextAfterTools . "\n" );
2727
2728
		$out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2729
			Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2730
2731
		$out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2732
			Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2733
2734
		if ( $this->mParserOutput ) {
2735
			$out->setLimitReportData( $this->mParserOutput->getLimitReportData() );
2736
		}
2737
2738
		$out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2739
2740 View Code Duplication
		if ( $this->isConflict ) {
2741
			try {
2742
				$this->showConflict();
2743
			} catch ( MWContentSerializationException $ex ) {
2744
				// this can't really happen, but be nice if it does.
2745
				$msg = wfMessage(
2746
					'content-failed-to-parse',
2747
					$this->contentModel,
2748
					$this->contentFormat,
2749
					$ex->getMessage()
2750
				);
2751
				$out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2752
			}
2753
		}
2754
2755
		// Set a hidden field so JS knows what edit form mode we are in
2756
		if ( $this->isConflict ) {
2757
			$mode = 'conflict';
2758
		} elseif ( $this->preview ) {
2759
			$mode = 'preview';
2760
		} elseif ( $this->diff ) {
2761
			$mode = 'diff';
2762
		} else {
2763
			$mode = 'text';
2764
		}
2765
		$out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2766
2767
		// Marker for detecting truncated form data.  This must be the last
2768
		// parameter sent in order to be of use, so do not move me.
2769
		$out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2770
		$out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2771
2772
		if ( !$user->getOption( 'previewontop' ) ) {
2773
			$this->displayPreviewArea( $previewOutput, false );
2774
		}
2775
2776
	}
2777
2778
	/**
2779
	 * Extract the section title from current section text, if any.
2780
	 *
2781
	 * @param string $text
2782
	 * @return string|bool String or false
2783
	 */
2784
	public static function extractSectionTitle( $text ) {
2785
		preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2786
		if ( !empty( $matches[2] ) ) {
2787
			global $wgParser;
2788
			return $wgParser->stripSectionName( trim( $matches[2] ) );
2789
		} else {
2790
			return false;
2791
		}
2792
	}
2793
2794
	/**
2795
	 * @return bool
2796
	 */
2797
	protected function showHeader() {
2798
		global $wgMaxArticleSize, $wgAllowUserCss, $wgAllowUserJs;
2799
2800
		$out = $this->context->getOutput();
2801
		$user = $this->context->getUser();
2802
2803
		if ( $this->mTitle->isTalkPage() ) {
2804
			$out->addWikiMsg( 'talkpagetext' );
2805
		}
2806
2807
		// Add edit notices
2808
		$editNotices = $this->mTitle->getEditNotices( $this->oldid );
2809
		if ( count( $editNotices ) ) {
2810
			$out->addHTML( implode( "\n", $editNotices ) );
2811
		} else {
2812
			$msg = wfMessage( 'editnotice-notext' );
2813
			if ( !$msg->isDisabled() ) {
2814
				$out->addHTML(
2815
					'<div class="mw-editnotice-notext">'
2816
					. $msg->parseAsBlock()
2817
					. '</div>'
2818
				);
2819
			}
2820
		}
2821
2822
		if ( $this->isConflict ) {
2823
			$out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2824
			$this->editRevId = $this->page->getLatest();
2825
		} else {
2826
			if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2827
				// We use $this->section to much before this and getVal('wgSection') directly in other places
2828
				// at this point we can't reset $this->section to '' to fallback to non-section editing.
2829
				// Someone is welcome to try refactoring though
2830
				$out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2831
				return false;
2832
			}
2833
2834
			if ( $this->section != '' && $this->section != 'new' ) {
2835
				if ( !$this->summary && !$this->preview && !$this->diff ) {
2836
					$sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2837
					if ( $sectionTitle !== false ) {
2838
						$this->summary = "/* $sectionTitle */ ";
2839
					}
2840
				}
2841
			}
2842
2843
			if ( $this->missingComment ) {
2844
				$out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2845
			}
2846
2847
			if ( $this->missingSummary && $this->section != 'new' ) {
2848
				$out->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2849
			}
2850
2851
			if ( $this->missingSummary && $this->section == 'new' ) {
2852
				$out->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2853
			}
2854
2855
			if ( $this->blankArticle ) {
2856
				$out->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2857
			}
2858
2859
			if ( $this->selfRedirect ) {
2860
				$out->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2861
			}
2862
2863
			if ( $this->hookError !== '' ) {
2864
				$out->addWikiText( $this->hookError );
2865
			}
2866
2867
			if ( !$this->checkUnicodeCompliantBrowser() ) {
2868
				$out->addWikiMsg( 'nonunicodebrowser' );
2869
			}
2870
2871
			if ( $this->section != 'new' ) {
2872
				$revision = $this->mArticle->getRevisionFetched();
2873
				if ( $revision ) {
2874
					// Let sysop know that this will make private content public if saved
2875
2876 View Code Duplication
					if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
2877
						$out->wrapWikiMsg(
2878
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2879
							'rev-deleted-text-permission'
2880
						);
2881
					} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2882
						$out->wrapWikiMsg(
2883
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2884
							'rev-deleted-text-view'
2885
						);
2886
					}
2887
2888
					if ( !$revision->isCurrent() ) {
2889
						$this->mArticle->setOldSubtitle( $revision->getId() );
2890
						$out->addWikiMsg( 'editingold' );
2891
					}
2892
				} elseif ( $this->mTitle->exists() ) {
2893
					// Something went wrong
2894
2895
					$out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2896
						[ 'missing-revision', $this->oldid ] );
2897
				}
2898
			}
2899
		}
2900
2901
		if ( wfReadOnly() ) {
2902
			$out->wrapWikiMsg(
2903
				"<div id=\"mw-read-only-warning\">\n$1\n</div>",
2904
				[ 'readonlywarning', wfReadOnlyReason() ]
2905
			);
2906
		} elseif ( $user->isAnon() ) {
2907
			if ( $this->formtype != 'preview' ) {
2908
				$out->wrapWikiMsg(
2909
					"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2910
					[ 'anoneditwarning',
2911
						// Log-in link
2912
						SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2913
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2914
						] ),
2915
						// Sign-up link
2916
						SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2917
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2918
						] )
2919
					]
2920
				);
2921
			} else {
2922
				$out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2923
					'anonpreviewwarning'
2924
				);
2925
			}
2926
		} else {
2927
			if ( $this->isCssJsSubpage ) {
2928
				# Check the skin exists
2929
				if ( $this->isWrongCaseCssJsPage ) {
2930
					$out->wrapWikiMsg(
2931
						"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2932
						[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2933
					);
2934
				}
2935
				if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
2936
					$out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
2937
						$this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
2938
					);
2939
					if ( $this->formtype !== 'preview' ) {
2940
						if ( $this->isCssSubpage && $wgAllowUserCss ) {
2941
							$out->wrapWikiMsg(
2942
								"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2943
								[ 'usercssyoucanpreview' ]
2944
							);
2945
						}
2946
2947
						if ( $this->isJsSubpage && $wgAllowUserJs ) {
2948
							$out->wrapWikiMsg(
2949
								"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2950
								[ 'userjsyoucanpreview' ]
2951
							);
2952
						}
2953
					}
2954
				}
2955
			}
2956
		}
2957
2958
		if ( $this->mTitle->isProtected( 'edit' ) &&
2959
			MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2960
		) {
2961
			# Is the title semi-protected?
2962
			if ( $this->mTitle->isSemiProtected() ) {
2963
				$noticeMsg = 'semiprotectedpagewarning';
2964
			} else {
2965
				# Then it must be protected based on static groups (regular)
2966
				$noticeMsg = 'protectedpagewarning';
2967
			}
2968
			LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
2969
				[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2970
		}
2971
		if ( $this->mTitle->isCascadeProtected() ) {
2972
			# Is this page under cascading protection from some source pages?
2973
			/** @var Title[] $cascadeSources */
2974
			list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2975
			$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2976
			$cascadeSourcesCount = count( $cascadeSources );
2977
			if ( $cascadeSourcesCount > 0 ) {
2978
				# Explain, and list the titles responsible
2979
				foreach ( $cascadeSources as $page ) {
2980
					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2981
				}
2982
			}
2983
			$notice .= '</div>';
2984
			$out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2985
		}
2986
		if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2987
			LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
2988
				[ 'lim' => 1,
2989
					'showIfEmpty' => false,
2990
					'msgKey' => [ 'titleprotectedwarning' ],
2991
					'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2992
		}
2993
2994
		if ( $this->contentLength === false ) {
2995
			$this->contentLength = strlen( $this->textbox1 );
2996
		}
2997
2998
		$lang = $this->context->getLanguage();
2999
		if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
3000
			$out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
3001
				[
3002
					'longpageerror',
3003
					$lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
3004
					$lang->formatNum( $wgMaxArticleSize )
3005
				]
3006
			);
3007
		} else {
3008
			if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
3009
				$out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
3010
					[
3011
						'longpage-hint',
3012
						$lang->formatSize( strlen( $this->textbox1 ) ),
3013
						strlen( $this->textbox1 )
3014
					]
3015
				);
3016
			}
3017
		}
3018
		# Add header copyright warning
3019
		$this->showHeaderCopyrightWarning();
3020
3021
		return true;
3022
	}
3023
3024
	/**
3025
	 * Standard summary input and label (wgSummary), abstracted so EditPage
3026
	 * subclasses may reorganize the form.
3027
	 * Note that you do not need to worry about the label's for=, it will be
3028
	 * inferred by the id given to the input. You can remove them both by
3029
	 * passing array( 'id' => false ) to $userInputAttrs.
3030
	 *
3031
	 * @param string $summary The value of the summary input
3032
	 * @param string $labelText The html to place inside the label
3033
	 * @param array $inputAttrs Array of attrs to use on the input
3034
	 * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
3035
	 *
3036
	 * @return array An array in the format array( $label, $input )
3037
	 */
3038
	function getSummaryInput( $summary = "", $labelText = null,
3039
		$inputAttrs = null, $spanLabelAttrs = null
3040
	) {
3041
		// Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
3042
		$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3043
			'id' => 'wpSummary',
3044
			'maxlength' => '200',
3045
			'tabindex' => '1',
3046
			'size' => 60,
3047
			'spellcheck' => 'true',
3048
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
3049
3050
		$spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3051
			'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3052
			'id' => "wpSummaryLabel"
3053
		];
3054
3055
		$label = null;
3056
		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...
3057
			$label = Xml::tags(
3058
				'label',
3059
				$inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3060
				$labelText
3061
			);
3062
			$label = Xml::tags( 'span', $spanLabelAttrs, $label );
3063
		}
3064
3065
		$input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3066
3067
		return [ $label, $input ];
3068
	}
3069
3070
	/**
3071
	 * @param bool $isSubjectPreview True if this is the section subject/title
3072
	 *   up top, or false if this is the comment summary
3073
	 *   down below the textarea
3074
	 * @param string $summary The text of the summary to display
3075
	 */
3076
	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3077
		# Add a class if 'missingsummary' is triggered to allow styling of the summary line
3078
		$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3079
		if ( $isSubjectPreview ) {
3080
			if ( $this->nosummary ) {
3081
				return;
3082
			}
3083
		} else {
3084
			if ( !$this->mShowSummaryField ) {
3085
				return;
3086
			}
3087
		}
3088
		$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3089
		list( $label, $input ) = $this->getSummaryInput(
3090
			$summary,
3091
			$labelText,
3092
			[ 'class' => $summaryClass ],
3093
			[]
3094
		);
3095
		$this->context->getOutput()->addHTML( "{$label} {$input}" );
3096
	}
3097
3098
	/**
3099
	 * @param bool $isSubjectPreview True if this is the section subject/title
3100
	 *   up top, or false if this is the comment summary
3101
	 *   down below the textarea
3102
	 * @param string $summary The text of the summary to display
3103
	 * @return string
3104
	 */
3105
	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3106
		// avoid spaces in preview, gets always trimmed on save
3107
		$summary = trim( $summary );
3108
		if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3109
			return "";
3110
		}
3111
3112
		global $wgParser;
3113
3114
		if ( $isSubjectPreview ) {
3115
			$summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3116
				->inContentLanguage()->text();
3117
		}
3118
3119
		$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3120
3121
		$summary = wfMessage( $message )->parse()
3122
			. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3123
		return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3124
	}
3125
3126
	protected function showFormBeforeText() {
3127
		$section = htmlspecialchars( $this->section );
3128
		$out = $this->context->getOutput();
3129
		$out->addHTML( <<<HTML
3130
<input type='hidden' value="{$section}" name="wpSection"/>
3131
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3132
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3133
<input type='hidden' value="{$this->editRevId}" name="editRevId" />
3134
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3135
3136
HTML
3137
		);
3138
		if ( !$this->checkUnicodeCompliantBrowser() ) {
3139
			$out->addHTML( Html::hidden( 'safemode', '1' ) );
3140
		}
3141
	}
3142
3143
	protected function showFormAfterText() {
3144
		/**
3145
		 * To make it harder for someone to slip a user a page
3146
		 * which submits an edit form to the wiki without their
3147
		 * knowledge, a random token is associated with the login
3148
		 * session. If it's not passed back with the submission,
3149
		 * we won't save the page, or render user JavaScript and
3150
		 * CSS previews.
3151
		 *
3152
		 * For anon editors, who may not have a session, we just
3153
		 * include the constant suffix to prevent editing from
3154
		 * broken text-mangling proxies.
3155
		 */
3156
		$token = $this->context->getUser()->getEditToken();
3157
		$this->context->getOutput()->addHTML(
3158
			"\n" . Html::hidden( "wpEditToken", $token ) . "\n"
3159
		);
3160
	}
3161
3162
	/**
3163
	 * Subpage overridable method for printing the form for page content editing
3164
	 * By default this simply outputs wpTextbox1
3165
	 * Subclasses can override this to provide a custom UI for editing;
3166
	 * be it a form, or simply wpTextbox1 with a modified content that will be
3167
	 * reverse modified when extracted from the post data.
3168
	 * Note that this is basically the inverse for importContentFormData
3169
	 */
3170
	protected function showContentForm() {
3171
		$this->showTextbox1();
3172
	}
3173
3174
	/**
3175
	 * Method to output wpTextbox1
3176
	 * The $textoverride method can be used by subclasses overriding showContentForm
3177
	 * to pass back to this method.
3178
	 *
3179
	 * @param array $customAttribs Array of html attributes to use in the textarea
3180
	 * @param string $textoverride Optional text to override $this->textarea1 with
3181
	 */
3182
	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3183
		if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3184
			$attribs = [ 'style' => 'display:none;' ];
3185
		} else {
3186
			$classes = []; // Textarea CSS
3187
			if ( $this->mTitle->isProtected( 'edit' ) &&
3188
				MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3189
			) {
3190
				# Is the title semi-protected?
3191
				if ( $this->mTitle->isSemiProtected() ) {
3192
					$classes[] = 'mw-textarea-sprotected';
3193
				} else {
3194
					# Then it must be protected based on static groups (regular)
3195
					$classes[] = 'mw-textarea-protected';
3196
				}
3197
				# Is the title cascade-protected?
3198
				if ( $this->mTitle->isCascadeProtected() ) {
3199
					$classes[] = 'mw-textarea-cprotected';
3200
				}
3201
			}
3202
3203
			$attribs = [ 'tabindex' => 1 ];
3204
3205
			if ( is_array( $customAttribs ) ) {
3206
				$attribs += $customAttribs;
3207
			}
3208
3209
			if ( count( $classes ) ) {
3210
				if ( isset( $attribs['class'] ) ) {
3211
					$classes[] = $attribs['class'];
3212
				}
3213
				$attribs['class'] = implode( ' ', $classes );
3214
			}
3215
		}
3216
3217
		$this->showTextbox(
3218
			$textoverride !== null ? $textoverride : $this->textbox1,
3219
			'wpTextbox1',
3220
			$attribs
3221
		);
3222
	}
3223
3224
	protected function showTextbox2() {
3225
		$this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3226
	}
3227
3228
	protected function showTextbox( $text, $name, $customAttribs = [] ) {
3229
		$wikitext = $this->safeUnicodeOutput( $text );
3230
		if ( strval( $wikitext ) !== '' ) {
3231
			// Ensure there's a newline at the end, otherwise adding lines
3232
			// is awkward.
3233
			// But don't add a newline if the ext is empty, or Firefox in XHTML
3234
			// mode will show an extra newline. A bit annoying.
3235
			$wikitext .= "\n";
3236
		}
3237
3238
		$user = $this->context->getUser();
3239
		$attribs = $customAttribs + [
3240
			'accesskey' => ',',
3241
			'id' => $name,
3242
			'cols' => $user->getIntOption( 'cols' ),
3243
			'rows' => $user->getIntOption( 'rows' ),
3244
			// Avoid PHP notices when appending preferences
3245
			// (appending allows customAttribs['style'] to still work).
3246
			'style' => ''
3247
		];
3248
3249
		$pageLang = $this->mTitle->getPageLanguage();
3250
		$attribs['lang'] = $pageLang->getHtmlCode();
3251
		$attribs['dir'] = $pageLang->getDir();
3252
3253
		$this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3254
	}
3255
3256
	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3257
		$classes = [];
3258
		if ( $isOnTop ) {
3259
			$classes[] = 'ontop';
3260
		}
3261
3262
		$attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3263
3264
		if ( $this->formtype != 'preview' ) {
3265
			$attribs['style'] = 'display: none;';
3266
		}
3267
3268
		$out = $this->context->getOutput();
3269
		$out->addHTML( Xml::openElement( 'div', $attribs ) );
3270
3271
		if ( $this->formtype == 'preview' ) {
3272
			$this->showPreview( $previewOutput );
3273
		} else {
3274
			// Empty content container for LivePreview
3275
			$pageViewLang = $this->mTitle->getPageViewLanguage();
3276
			$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3277
				'class' => 'mw-content-' . $pageViewLang->getDir() ];
3278
			$out->addHTML( Html::rawElement( 'div', $attribs ) );
3279
		}
3280
3281
		$out->addHTML( '</div>' );
3282
3283 View Code Duplication
		if ( $this->formtype == 'diff' ) {
3284
			try {
3285
				$this->showDiff();
3286
			} catch ( MWContentSerializationException $ex ) {
3287
				$msg = wfMessage(
3288
					'content-failed-to-parse',
3289
					$this->contentModel,
3290
					$this->contentFormat,
3291
					$ex->getMessage()
3292
				);
3293
				$out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3294
			}
3295
		}
3296
	}
3297
3298
	/**
3299
	 * Append preview output to $wgOut.
3300
	 * Includes category rendering if this is a category page.
3301
	 *
3302
	 * @param string $text The HTML to be output for the preview.
3303
	 */
3304
	protected function showPreview( $text ) {
3305
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3306
			$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...
3307
		}
3308
		$out = $this->context->getOutput();
3309
		# This hook seems slightly odd here, but makes things more
3310
		# consistent for extensions.
3311
		Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3312
		$out->addHTML( $text );
3313
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3314
			$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...
3315
		}
3316
	}
3317
3318
	/**
3319
	 * Get a diff between the current contents of the edit box and the
3320
	 * version of the page we're editing from.
3321
	 *
3322
	 * If this is a section edit, we'll replace the section as for final
3323
	 * save and then make a comparison.
3324
	 */
3325
	function showDiff() {
3326
		global $wgContLang;
3327
3328
		$oldtitlemsg = 'currentrev';
3329
		# if message does not exist, show diff against the preloaded default
3330
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3331
			$oldtext = $this->mTitle->getDefaultMessageText();
3332
			if ( $oldtext !== false ) {
3333
				$oldtitlemsg = 'defaultmessagetext';
3334
				$oldContent = $this->toEditContent( $oldtext );
3335
			} else {
3336
				$oldContent = null;
3337
			}
3338
		} else {
3339
			$oldContent = $this->getCurrentContent();
3340
		}
3341
3342
		$textboxContent = $this->toEditContent( $this->textbox1 );
3343
		if ( $this->editRevId !== null ) {
3344
			$newContent = $this->page->replaceSectionAtRev(
3345
				$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 3342 can also be of type false or null; however, WikiPage::replaceSectionAtRev() 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...
3346
			);
3347
		} else {
3348
			$newContent = $this->page->replaceSectionContent(
3349
				$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 3342 can also be of type false or null; however, WikiPage::replaceSectionContent() 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...
3350
			);
3351
		}
3352
3353
		if ( $newContent ) {
3354
			ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3355
			Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3356
3357
			$user = $this->context->getUser();
3358
			$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
3359
			$newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3360
		}
3361
3362
		if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3363
			$oldtitle = wfMessage( $oldtitlemsg )->parse();
3364
			$newtitle = wfMessage( 'yourtext' )->parse();
3365
3366
			if ( !$oldContent ) {
3367
				$oldContent = $newContent->getContentHandler()->makeEmptyContent();
3368
			}
3369
3370
			if ( !$newContent ) {
3371
				$newContent = $oldContent->getContentHandler()->makeEmptyContent();
3372
			}
3373
3374
			$de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3375
			$de->setContent( $oldContent, $newContent );
3376
3377
			$difftext = $de->getDiff( $oldtitle, $newtitle );
3378
			$de->showDiffStyle();
3379
		} else {
3380
			$difftext = '';
3381
		}
3382
3383
		$this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3384
	}
3385
3386
	/**
3387
	 * Show the header copyright warning.
3388
	 */
3389
	protected function showHeaderCopyrightWarning() {
3390
		$msg = 'editpage-head-copy-warn';
3391
		if ( !wfMessage( $msg )->isDisabled() ) {
3392
			$this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3393
				'editpage-head-copy-warn' );
3394
		}
3395
	}
3396
3397
	/**
3398
	 * Give a chance for site and per-namespace customizations of
3399
	 * terms of service summary link that might exist separately
3400
	 * from the copyright notice.
3401
	 *
3402
	 * This will display between the save button and the edit tools,
3403
	 * so should remain short!
3404
	 */
3405
	protected function showTosSummary() {
3406
		$msg = 'editpage-tos-summary';
3407
		Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3408
		if ( !wfMessage( $msg )->isDisabled() ) {
3409
			$out = $this->context->getOutput();
3410
			$out->addHTML( '<div class="mw-tos-summary">' );
3411
			$out->addWikiMsg( $msg );
3412
			$out->addHTML( '</div>' );
3413
		}
3414
	}
3415
3416
	protected function showEditTools() {
3417
		$this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3418
			wfMessage( 'edittools' )->inContentLanguage()->parse() .
3419
			'</div>' );
3420
	}
3421
3422
	/**
3423
	 * Get the copyright warning
3424
	 *
3425
	 * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3426
	 * @return string
3427
	 */
3428
	protected function getCopywarn() {
3429
		return self::getCopyrightWarning( $this->mTitle );
3430
	}
3431
3432
	/**
3433
	 * Get the copyright warning, by default returns wikitext
3434
	 *
3435
	 * @param Title $title
3436
	 * @param string $format Output format, valid values are any function of a Message object
3437
	 * @return string
3438
	 */
3439
	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3440
		global $wgRightsText;
3441
		if ( $wgRightsText ) {
3442
			$copywarnMsg = [ 'copyrightwarning',
3443
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3444
				$wgRightsText ];
3445
		} else {
3446
			$copywarnMsg = [ 'copyrightwarning2',
3447
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3448
		}
3449
		// Allow for site and per-namespace customization of contribution/copyright notice.
3450
		Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3451
3452
		return "<div id=\"editpage-copywarn\">\n" .
3453
			call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3454
	}
3455
3456
	/**
3457
	 * Get the Limit report for page previews
3458
	 *
3459
	 * @since 1.22
3460
	 * @param ParserOutput $output ParserOutput object from the parse
3461
	 * @return string HTML
3462
	 */
3463
	public static function getPreviewLimitReport( $output ) {
3464
		if ( !$output || !$output->getLimitReportData() ) {
3465
			return '';
3466
		}
3467
3468
		return ResourceLoader::makeInlineScript(
3469
			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...
3470
				[ 'wgPageParseReport' => $output->getLimitReportData() ],
3471
				true
3472
			)
3473
		);
3474
	}
3475
3476
	protected function showStandardInputs( &$tabindex = 2 ) {
3477
		$out = $this->context->getOutput();
3478
		$out->addHTML( "<div class='editOptions'>\n" );
3479
3480 View Code Duplication
		if ( $this->section != 'new' ) {
3481
			$this->showSummaryInput( false, $this->summary );
3482
			$out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3483
		}
3484
3485
		$checkboxes = $this->getCheckboxes( $tabindex,
3486
			[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3487
		$out->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3488
3489
		// Show copyright warning.
3490
		$out->addWikiText( $this->getCopywarn() );
3491
		$out->addHTML( $this->editFormTextAfterWarn );
3492
3493
		$out->addHTML( "<div class='editButtons'>\n" );
3494
		$out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3495
3496
		$cancel = $this->getCancelLink();
3497
		if ( $cancel !== '' ) {
3498
			$cancel .= Html::element( 'span',
3499
				[ 'class' => 'mw-editButtons-pipe-separator' ],
3500
				wfMessage( 'pipe-separator' )->text() );
3501
		}
3502
3503
		$message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3504
		$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3505
		$attrs = [
3506
			'target' => 'helpwindow',
3507
			'href' => $edithelpurl,
3508
		];
3509
		$edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3510
			$attrs, [ 'mw-ui-quiet' ] ) .
3511
			wfMessage( 'word-separator' )->escaped() .
3512
			wfMessage( 'newwindow' )->parse();
3513
3514
		$out->addHTML( "	<span class='cancelLink'>{$cancel}</span>\n" );
3515
		$out->addHTML( "	<span class='editHelp'>{$edithelp}</span>\n" );
3516
		$out->addHTML( "</div><!-- editButtons -->\n" );
3517
3518
		Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3519
3520
		$out->addHTML( "</div><!-- editOptions -->\n" );
3521
	}
3522
3523
	/**
3524
	 * Show an edit conflict. textbox1 is already shown in showEditForm().
3525
	 * If you want to use another entry point to this function, be careful.
3526
	 */
3527
	protected function showConflict() {
3528
		$out = $this->context->getOutput();
3529
		if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$out ] ) ) {
3530
			$stats = $this->context->getStats();
3531
			$stats->increment( 'edit.failures.conflict' );
3532
			// Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
3533
			if (
3534
				$this->mTitle->getNamespace() >= NS_MAIN &&
3535
				$this->mTitle->getNamespace() <= NS_CATEGORY_TALK
3536
			) {
3537
				$stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
3538
			}
3539
3540
			$out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3541
3542
			$content1 = $this->toEditContent( $this->textbox1 );
3543
			$content2 = $this->toEditContent( $this->textbox2 );
3544
3545
			$handler = ContentHandler::getForModelID( $this->contentModel );
3546
			$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3547
			$de->setContent( $content2, $content1 );
0 ignored issues
show
Bug introduced by
It seems like $content2 defined by $this->toEditContent($this->textbox2) on line 3543 can also be of type false or null; however, DifferenceEngine::setContent() 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...
Bug introduced by
It seems like $content1 defined by $this->toEditContent($this->textbox1) on line 3542 can also be of type false or null; however, DifferenceEngine::setContent() 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...
3548
			$de->showDiff(
3549
				wfMessage( 'yourtext' )->parse(),
3550
				wfMessage( 'storedversion' )->text()
3551
			);
3552
3553
			$out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3554
			$this->showTextbox2();
3555
		}
3556
	}
3557
3558
	/**
3559
	 * @return string
3560
	 */
3561
	public function getCancelLink() {
3562
		$cancelParams = [];
3563
		if ( !$this->isConflict && $this->oldid > 0 ) {
3564
			$cancelParams['oldid'] = $this->oldid;
3565
		} elseif ( $this->getContextTitle()->isRedirect() ) {
3566
			$cancelParams['redirect'] = 'no';
3567
		}
3568
		$attrs = [ 'id' => 'mw-editform-cancel' ];
3569
3570
		return Linker::linkKnown(
3571
			$this->getContextTitle(),
3572
			wfMessage( 'cancel' )->parse(),
3573
			Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3574
			$cancelParams
3575
		);
3576
	}
3577
3578
	/**
3579
	 * Returns the URL to use in the form's action attribute.
3580
	 * This is used by EditPage subclasses when simply customizing the action
3581
	 * variable in the constructor is not enough. This can be used when the
3582
	 * EditPage lives inside of a Special page rather than a custom page action.
3583
	 *
3584
	 * @param Title $title Title object for which is being edited (where we go to for &action= links)
3585
	 * @return string
3586
	 */
3587
	protected function getActionURL( Title $title ) {
3588
		return $title->getLocalURL( [ 'action' => $this->action ] );
3589
	}
3590
3591
	/**
3592
	 * Check if a page was deleted while the user was editing it, before submit.
3593
	 * Note that we rely on the logging table, which hasn't been always there,
3594
	 * but that doesn't matter, because this only applies to brand new
3595
	 * deletes.
3596
	 * @return bool
3597
	 */
3598
	protected function wasDeletedSinceLastEdit() {
3599
		if ( $this->deletedSinceEdit !== null ) {
3600
			return $this->deletedSinceEdit;
3601
		}
3602
3603
		$this->deletedSinceEdit = false;
3604
3605
		if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3606
			$this->lastDelete = $this->getLastDelete();
3607
			if ( $this->lastDelete ) {
3608
				$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3609
				if ( $deleteTime > $this->starttime ) {
3610
					$this->deletedSinceEdit = true;
3611
				}
3612
			}
3613
		}
3614
3615
		return $this->deletedSinceEdit;
3616
	}
3617
3618
	/**
3619
	 * @return bool|stdClass
3620
	 */
3621
	protected function getLastDelete() {
3622
		$dbr = wfGetDB( DB_SLAVE );
3623
		$data = $dbr->selectRow(
3624
			[ 'logging', 'user' ],
3625
			[
3626
				'log_type',
3627
				'log_action',
3628
				'log_timestamp',
3629
				'log_user',
3630
				'log_namespace',
3631
				'log_title',
3632
				'log_comment',
3633
				'log_params',
3634
				'log_deleted',
3635
				'user_name'
3636
			], [
3637
				'log_namespace' => $this->mTitle->getNamespace(),
3638
				'log_title' => $this->mTitle->getDBkey(),
3639
				'log_type' => 'delete',
3640
				'log_action' => 'delete',
3641
				'user_id=log_user'
3642
			],
3643
			__METHOD__,
3644
			[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3645
		);
3646
		// Quick paranoid permission checks...
3647
		if ( is_object( $data ) ) {
3648
			if ( $data->log_deleted & LogPage::DELETED_USER ) {
3649
				$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3650
			}
3651
3652
			if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3653
				$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3654
			}
3655
		}
3656
3657
		return $data;
3658
	}
3659
3660
	/**
3661
	 * Get the rendered text for previewing.
3662
	 * @throws MWException
3663
	 * @return string
3664
	 */
3665
	function getPreviewText() {
3666
		global $wgRawHtml, $wgAllowUserCss, $wgAllowUserJs;
3667
3668
		$stats = $this->context->getStats();
3669
		$out = $this->context->getOutput();
3670
3671
		if ( $wgRawHtml && !$this->mTokenOk ) {
3672
			// Could be an offsite preview attempt. This is very unsafe if
3673
			// HTML is enabled, as it could be an attack.
3674
			$parsedNote = '';
3675
			if ( $this->textbox1 !== '' ) {
3676
				// Do not put big scary notice, if previewing the empty
3677
				// string, which happens when you initially edit
3678
				// a category page, due to automatic preview-on-open.
3679
				$parsedNote = $out->parse( "<div class='previewnote'>" .
3680
					wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3681
			}
3682
			$stats->increment( 'edit.failures.session_loss' );
3683
			return $parsedNote;
3684
		}
3685
3686
		$note = '';
3687
3688
		try {
3689
			$content = $this->toEditContent( $this->textbox1 );
3690
3691
			$previewHTML = '';
3692
			if ( !Hooks::run(
3693
				'AlternateEditPreview',
3694
				[ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3695
			) {
3696
				return $previewHTML;
3697
			}
3698
3699
			# provide a anchor link to the editform
3700
			$continueEditing = '<span class="mw-continue-editing">' .
3701
				'[[#' . self::EDITFORM_ID . '|' . $this->context->getLanguage()->getArrow() . ' ' .
3702
				wfMessage( 'continue-editing' )->text() . ']]</span>';
3703
			if ( $this->mTriedSave && !$this->mTokenOk ) {
3704
				if ( $this->mTokenOkExceptSuffix ) {
3705
					$note = wfMessage( 'token_suffix_mismatch' )->plain();
3706
					$stats->increment( 'edit.failures.bad_token' );
3707
				} else {
3708
					$note = wfMessage( 'session_fail_preview' )->plain();
3709
					$stats->increment( 'edit.failures.session_loss' );
3710
				}
3711
			} elseif ( $this->incompleteForm ) {
3712
				$note = wfMessage( 'edit_form_incomplete' )->plain();
3713
				if ( $this->mTriedSave ) {
3714
					$stats->increment( 'edit.failures.incomplete_form' );
3715
				}
3716
			} else {
3717
				$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3718
			}
3719
3720
			# don't parse non-wikitext pages, show message about preview
3721
			if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3722
				if ( $this->mTitle->isCssJsSubpage() ) {
3723
					$level = 'user';
3724
				} elseif ( $this->mTitle->isCssOrJsPage() ) {
3725
					$level = 'site';
3726
				} else {
3727
					$level = false;
3728
				}
3729
3730
				if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3731
					$format = 'css';
3732
					if ( $level === 'user' && !$wgAllowUserCss ) {
3733
						$format = false;
3734
					}
3735
				} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3736
					$format = 'js';
3737
					if ( $level === 'user' && !$wgAllowUserJs ) {
3738
						$format = false;
3739
					}
3740
				} else {
3741
					$format = false;
3742
				}
3743
3744
				# Used messages to make sure grep find them:
3745
				# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3746
				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...
3747
					$note = "<div id='mw-{$level}{$format}preview'>" .
3748
						wfMessage( "{$level}{$format}preview" )->text() .
3749
						' ' . $continueEditing . "</div>";
3750
				}
3751
			}
3752
3753
			# If we're adding a comment, we need to show the
3754
			# summary as the headline
3755
			if ( $this->section === "new" && $this->summary !== "" ) {
3756
				$content = $content->addSectionHeader( $this->summary );
3757
			}
3758
3759
			$hook_args = [ $this, &$content ];
3760
			ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3761
			Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3762
3763
			$parserResult = $this->doPreviewParse( $content );
3764
			$parserOutput = $parserResult['parserOutput'];
3765
			$previewHTML = $parserResult['html'];
3766
			$this->mParserOutput = $parserOutput;
3767
			$out->addParserOutputMetadata( $parserOutput );
3768
3769
			if ( count( $parserOutput->getWarnings() ) ) {
3770
				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3771
			}
3772
3773
		} catch ( MWContentSerializationException $ex ) {
3774
			$m = wfMessage(
3775
				'content-failed-to-parse',
3776
				$this->contentModel,
3777
				$this->contentFormat,
3778
				$ex->getMessage()
3779
			);
3780
			$note .= "\n\n" . $m->parse();
3781
			$previewHTML = '';
3782
		}
3783
3784
		if ( $this->isConflict ) {
3785
			$conflict = '<h2 id="mw-previewconflict">'
3786
				. wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3787
		} else {
3788
			$conflict = '<hr />';
3789
		}
3790
3791
		$previewhead = "<div class='previewnote'>\n" .
3792
			'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3793
			$out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3794
3795
		$pageViewLang = $this->mTitle->getPageViewLanguage();
3796
		$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3797
			'class' => 'mw-content-' . $pageViewLang->getDir() ];
3798
		$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3799
3800
		return $previewhead . $previewHTML . $this->previewTextAfterContent;
3801
	}
3802
3803
	/**
3804
	 * Get parser options for a preview
3805
	 * @return ParserOptions
3806
	 */
3807
	protected function getPreviewParserOptions() {
3808
		$parserOptions = $this->page->makeParserOptions( $this->context );
3809
		$parserOptions->setIsPreview( true );
3810
		$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3811
		$parserOptions->enableLimitReport();
3812
		return $parserOptions;
3813
	}
3814
3815
	/**
3816
	 * Parse the page for a preview. Subclasses may override this class, in order
3817
	 * to parse with different options, or to otherwise modify the preview HTML.
3818
	 *
3819
	 * @param Content $content The page content
3820
	 * @return array with keys:
3821
	 *   - parserOutput: The ParserOutput object
3822
	 *   - html: The HTML to be displayed
3823
	 */
3824
	protected function doPreviewParse( Content $content ) {
3825
		$user = $this->context->getUser();
3826
		$parserOptions = $this->getPreviewParserOptions();
3827
		$pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
3828
		$scopedCallback = $parserOptions->setupFakeRevision(
3829
			$this->mTitle, $pstContent, $user );
3830
		$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3831
		ScopedCallback::consume( $scopedCallback );
3832
		$parserOutput->setEditSectionTokens( false ); // no section edit links
3833
		return [
3834
			'parserOutput' => $parserOutput,
3835
			'html' => $parserOutput->getText() ];
3836
	}
3837
3838
	/**
3839
	 * @return array
3840
	 */
3841
	function getTemplates() {
3842
		if ( $this->preview || $this->section != '' ) {
3843
			$templates = [];
3844
			if ( !isset( $this->mParserOutput ) ) {
3845
				return $templates;
3846
			}
3847
			foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3848
				foreach ( array_keys( $template ) as $dbk ) {
3849
					$templates[] = Title::makeTitle( $ns, $dbk );
3850
				}
3851
			}
3852
			return $templates;
3853
		} else {
3854
			return $this->mTitle->getTemplateLinksFrom();
3855
		}
3856
	}
3857
3858
	/**
3859
	 * Shows a bulletin board style toolbar for common editing functions.
3860
	 * It can be disabled in the user preferences.
3861
	 *
3862
	 * @param Title $title Title object for the page being edited (optional)
3863
	 * @return string
3864
	 */
3865
	static function getEditToolbar( $title = null ) {
3866
		global $wgContLang, $wgOut;
3867
		global $wgEnableUploads, $wgForeignFileRepos;
3868
3869
		$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3870
		$showSignature = true;
3871
		if ( $title ) {
3872
			$showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3873
		}
3874
3875
		/**
3876
		 * $toolarray is an array of arrays each of which includes the
3877
		 * opening tag, the closing tag, optionally a sample text that is
3878
		 * inserted between the two when no selection is highlighted
3879
		 * and.  The tip text is shown when the user moves the mouse
3880
		 * over the button.
3881
		 *
3882
		 * Images are defined in ResourceLoaderEditToolbarModule.
3883
		 */
3884
		$toolarray = [
3885
			[
3886
				'id'     => 'mw-editbutton-bold',
3887
				'open'   => '\'\'\'',
3888
				'close'  => '\'\'\'',
3889
				'sample' => wfMessage( 'bold_sample' )->text(),
3890
				'tip'    => wfMessage( 'bold_tip' )->text(),
3891
			],
3892
			[
3893
				'id'     => 'mw-editbutton-italic',
3894
				'open'   => '\'\'',
3895
				'close'  => '\'\'',
3896
				'sample' => wfMessage( 'italic_sample' )->text(),
3897
				'tip'    => wfMessage( 'italic_tip' )->text(),
3898
			],
3899
			[
3900
				'id'     => 'mw-editbutton-link',
3901
				'open'   => '[[',
3902
				'close'  => ']]',
3903
				'sample' => wfMessage( 'link_sample' )->text(),
3904
				'tip'    => wfMessage( 'link_tip' )->text(),
3905
			],
3906
			[
3907
				'id'     => 'mw-editbutton-extlink',
3908
				'open'   => '[',
3909
				'close'  => ']',
3910
				'sample' => wfMessage( 'extlink_sample' )->text(),
3911
				'tip'    => wfMessage( 'extlink_tip' )->text(),
3912
			],
3913
			[
3914
				'id'     => 'mw-editbutton-headline',
3915
				'open'   => "\n== ",
3916
				'close'  => " ==\n",
3917
				'sample' => wfMessage( 'headline_sample' )->text(),
3918
				'tip'    => wfMessage( 'headline_tip' )->text(),
3919
			],
3920
			$imagesAvailable ? [
3921
				'id'     => 'mw-editbutton-image',
3922
				'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3923
				'close'  => ']]',
3924
				'sample' => wfMessage( 'image_sample' )->text(),
3925
				'tip'    => wfMessage( 'image_tip' )->text(),
3926
			] : false,
3927
			$imagesAvailable ? [
3928
				'id'     => 'mw-editbutton-media',
3929
				'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3930
				'close'  => ']]',
3931
				'sample' => wfMessage( 'media_sample' )->text(),
3932
				'tip'    => wfMessage( 'media_tip' )->text(),
3933
			] : false,
3934
			[
3935
				'id'     => 'mw-editbutton-nowiki',
3936
				'open'   => "<nowiki>",
3937
				'close'  => "</nowiki>",
3938
				'sample' => wfMessage( 'nowiki_sample' )->text(),
3939
				'tip'    => wfMessage( 'nowiki_tip' )->text(),
3940
			],
3941
			$showSignature ? [
3942
				'id'     => 'mw-editbutton-signature',
3943
				'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
3944
				'close'  => '',
3945
				'sample' => '',
3946
				'tip'    => wfMessage( 'sig_tip' )->text(),
3947
			] : false,
3948
			[
3949
				'id'     => 'mw-editbutton-hr',
3950
				'open'   => "\n----\n",
3951
				'close'  => '',
3952
				'sample' => '',
3953
				'tip'    => wfMessage( 'hr_tip' )->text(),
3954
			]
3955
		];
3956
3957
		$script = 'mw.loader.using("mediawiki.toolbar", function () {';
3958
		foreach ( $toolarray as $tool ) {
3959
			if ( !$tool ) {
3960
				continue;
3961
			}
3962
3963
			$params = [
3964
				// Images are defined in ResourceLoaderEditToolbarModule
3965
				false,
3966
				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3967
				// Older browsers show a "speedtip" type message only for ALT.
3968
				// Ideally these should be different, realistically they
3969
				// probably don't need to be.
3970
				$tool['tip'],
3971
				$tool['open'],
3972
				$tool['close'],
3973
				$tool['sample'],
3974
				$tool['id'],
3975
			];
3976
3977
			$script .= Xml::encodeJsCall(
3978
				'mw.toolbar.addButton',
3979
				$params,
3980
				ResourceLoader::inDebugMode()
3981
			);
3982
		}
3983
3984
		$script .= '});';
3985
		$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3986
3987
		$toolbar = '<div id="toolbar"></div>';
3988
3989
		Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3990
3991
		return $toolbar;
3992
	}
3993
3994
	/**
3995
	 * Returns an array of html code of the following checkboxes:
3996
	 * minor and watch
3997
	 *
3998
	 * @param int $tabindex Current tabindex
3999
	 * @param array $checked Array of checkbox => bool, where bool indicates the checked
4000
	 *                 status of the checkbox
4001
	 *
4002
	 * @return array
4003
	 */
4004
	public function getCheckboxes( &$tabindex, $checked ) {
4005
		global $wgUseMediaWikiUIEverywhere;
4006
4007
		$checkboxes = [];
4008
		$user = $this->context->getUser();
4009
4010
		// don't show the minor edit checkbox if it's a new page or section
4011
		if ( !$this->isNew ) {
4012
			$checkboxes['minor'] = '';
4013
			$minorLabel = wfMessage( 'minoredit' )->parse();
4014 View Code Duplication
			if ( $user->isAllowed( 'minoredit' ) ) {
4015
				$attribs = [
4016
					'tabindex' => ++$tabindex,
4017
					'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
4018
					'id' => 'wpMinoredit',
4019
				];
4020
				$minorEditHtml =
4021
					Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
4022
					"&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
4023
					Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
4024
					">{$minorLabel}</label>";
4025
4026
				if ( $wgUseMediaWikiUIEverywhere ) {
4027
					$checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4028
						$minorEditHtml .
4029
					Html::closeElement( 'div' );
4030
				} else {
4031
					$checkboxes['minor'] = $minorEditHtml;
4032
				}
4033
			}
4034
		}
4035
4036
		$watchLabel = wfMessage( 'watchthis' )->parse();
4037
		$checkboxes['watch'] = '';
4038 View Code Duplication
		if ( $user->isLoggedIn() ) {
4039
			$attribs = [
4040
				'tabindex' => ++$tabindex,
4041
				'accesskey' => wfMessage( 'accesskey-watch' )->text(),
4042
				'id' => 'wpWatchthis',
4043
			];
4044
			$watchThisHtml =
4045
				Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
4046
				"&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
4047
				Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
4048
				">{$watchLabel}</label>";
4049
			if ( $wgUseMediaWikiUIEverywhere ) {
4050
				$checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4051
					$watchThisHtml .
4052
					Html::closeElement( 'div' );
4053
			} else {
4054
				$checkboxes['watch'] = $watchThisHtml;
4055
			}
4056
		}
4057
		Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
4058
		return $checkboxes;
4059
	}
4060
4061
	/**
4062
	 * Returns an array of html code of the following buttons:
4063
	 * save, diff, preview and live
4064
	 *
4065
	 * @param int $tabindex Current tabindex
4066
	 *
4067
	 * @return array
4068
	 */
4069
	public function getEditButtons( &$tabindex ) {
4070
		$buttons = [];
4071
4072
		$labelAsPublish = $this->mArticle->getContext()->getConfig()->get( 'EditButtonPublishNotSave' );
4073
		if ( $labelAsPublish ) {
4074
			$buttonLabelKey = $this->isNew ? 'publishpage' : 'publishchanges';
4075
		} else {
4076
			$buttonLabelKey = $this->isNew ? 'savearticle' : 'savechanges';
4077
		}
4078
		$buttonLabel = wfMessage( $buttonLabelKey )->text();
4079
		$attribs = [
4080
			'id' => 'wpSave',
4081
			'name' => 'wpSave',
4082
			'tabindex' => ++$tabindex,
4083
		] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4084
		$buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-constructive' ] );
4085
4086
		++$tabindex; // use the same for preview and live preview
4087
		$attribs = [
4088
			'id' => 'wpPreview',
4089
			'name' => 'wpPreview',
4090
			'tabindex' => $tabindex,
4091
		] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4092
		$buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4093
			$attribs );
4094
		$buttons['live'] = '';
4095
4096
		$attribs = [
4097
			'id' => 'wpDiff',
4098
			'name' => 'wpDiff',
4099
			'tabindex' => ++$tabindex,
4100
		] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4101
		$buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4102
			$attribs );
4103
4104
		Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4105
		return $buttons;
4106
	}
4107
4108
	/**
4109
	 * Creates a basic error page which informs the user that
4110
	 * they have attempted to edit a nonexistent section.
4111
	 */
4112
	function noSuchSectionPage() {
4113
		$out = $this->context->getOutput();
4114
		$out->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4115
4116
		$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4117
		Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4118
		$out->addHTML( $res );
4119
4120
		$out->returnToMain( false, $this->mTitle );
4121
	}
4122
4123
	/**
4124
	 * Show "your edit contains spam" page with your diff and text
4125
	 *
4126
	 * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
4127
	 */
4128
	public function spamPageWithContent( $match = false ) {
4129
		$this->textbox2 = $this->textbox1;
4130
4131
		if ( is_array( $match ) ) {
4132
			$match = $this->context->getLanguage()->listToText( $match );
4133
		}
4134
		$out = $this->context->getOutput();
4135
		$out->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4136
4137
		$out->addHTML( '<div id="spamprotected">' );
4138
		$out->addWikiMsg( 'spamprotectiontext' );
4139
		if ( $match ) {
4140
			$out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
0 ignored issues
show
Bug introduced by
It seems like $match defined by parameter $match on line 4128 can also be of type boolean; however, wfEscapeWikiText() 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...
4141
		}
4142
		$out->addHTML( '</div>' );
4143
4144
		$out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4145
		$this->showDiff();
4146
4147
		$out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4148
		$this->showTextbox2();
4149
4150
		$out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4151
	}
4152
4153
	/**
4154
	 * Check if the browser is on a blacklist of user-agents known to
4155
	 * mangle UTF-8 data on form submission. Returns true if Unicode
4156
	 * should make it through, false if it's known to be a problem.
4157
	 * @return bool
4158
	 */
4159
	private function checkUnicodeCompliantBrowser() {
4160
		global $wgBrowserBlackList;
4161
4162
		$currentbrowser = $this->context->getRequest()->getHeader( 'User-Agent' );
4163
		if ( $currentbrowser === false ) {
4164
			// No User-Agent header sent? Trust it by default...
4165
			return true;
4166
		}
4167
4168
		foreach ( $wgBrowserBlackList as $browser ) {
4169
			if ( preg_match( $browser, $currentbrowser ) ) {
4170
				return false;
4171
			}
4172
		}
4173
		return true;
4174
	}
4175
4176
	/**
4177
	 * Filter an input field through a Unicode de-armoring process if it
4178
	 * came from an old browser with known broken Unicode editing issues.
4179
	 *
4180
	 * @param WebRequest $request
4181
	 * @param string $field
4182
	 * @return string
4183
	 */
4184
	protected function safeUnicodeInput( $request, $field ) {
4185
		$text = rtrim( $request->getText( $field ) );
4186
		return $request->getBool( 'safemode' )
4187
			? $this->unmakeSafe( $text )
4188
			: $text;
4189
	}
4190
4191
	/**
4192
	 * Filter an output field through a Unicode armoring process if it is
4193
	 * going to an old browser with known broken Unicode editing issues.
4194
	 *
4195
	 * @param string $text
4196
	 * @return string
4197
	 */
4198
	protected function safeUnicodeOutput( $text ) {
4199
		return $this->checkUnicodeCompliantBrowser()
4200
			? $text
4201
			: $this->makesafe( $text );
4202
	}
4203
4204
	/**
4205
	 * A number of web browsers are known to corrupt non-ASCII characters
4206
	 * in a UTF-8 text editing environment. To protect against this,
4207
	 * detected browsers will be served an armored version of the text,
4208
	 * with non-ASCII chars converted to numeric HTML character references.
4209
	 *
4210
	 * Preexisting such character references will have a 0 added to them
4211
	 * to ensure that round-trips do not alter the original data.
4212
	 *
4213
	 * @param string $invalue
4214
	 * @return string
4215
	 */
4216
	private function makeSafe( $invalue ) {
4217
		// Armor existing references for reversibility.
4218
		$invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4219
4220
		$bytesleft = 0;
4221
		$result = "";
4222
		$working = 0;
4223
		$valueLength = strlen( $invalue );
4224
		for ( $i = 0; $i < $valueLength; $i++ ) {
4225
			$bytevalue = ord( $invalue[$i] );
4226
			if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4227
				$result .= chr( $bytevalue );
4228
				$bytesleft = 0;
4229
			} elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4230
				$working = $working << 6;
4231
				$working += ( $bytevalue & 0x3F );
4232
				$bytesleft--;
4233
				if ( $bytesleft <= 0 ) {
4234
					$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4235
				}
4236
			} elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4237
				$working = $bytevalue & 0x1F;
4238
				$bytesleft = 1;
4239
			} elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4240
				$working = $bytevalue & 0x0F;
4241
				$bytesleft = 2;
4242
			} else { // 1111 0xxx
4243
				$working = $bytevalue & 0x07;
4244
				$bytesleft = 3;
4245
			}
4246
		}
4247
		return $result;
4248
	}
4249
4250
	/**
4251
	 * Reverse the previously applied transliteration of non-ASCII characters
4252
	 * back to UTF-8. Used to protect data from corruption by broken web browsers
4253
	 * as listed in $wgBrowserBlackList.
4254
	 *
4255
	 * @param string $invalue
4256
	 * @return string
4257
	 */
4258
	private function unmakeSafe( $invalue ) {
4259
		$result = "";
4260
		$valueLength = strlen( $invalue );
4261
		for ( $i = 0; $i < $valueLength; $i++ ) {
4262
			if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4263
				$i += 3;
4264
				$hexstring = "";
4265
				do {
4266
					$hexstring .= $invalue[$i];
4267
					$i++;
4268
				} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4269
4270
				// Do some sanity checks. These aren't needed for reversibility,
4271
				// but should help keep the breakage down if the editor
4272
				// breaks one of the entities whilst editing.
4273
				if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4274
					$codepoint = hexdec( $hexstring );
4275
					$result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4276
				} else {
4277
					$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4278
				}
4279
			} else {
4280
				$result .= substr( $invalue, $i, 1 );
4281
			}
4282
		}
4283
		// reverse the transform that we made for reversibility reasons.
4284
		return strtr( $result, [ "&#x0" => "&#x" ] );
4285
	}
4286
}
4287