Completed
Branch master (6ee3f9)
by
unknown
29:15
created

EditPage::checkUnicodeCompliantBrowser()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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