Completed
Branch master (1655eb)
by
unknown
22:24
created

EditPage::addPageProtectionWarningHeaders()   D

Complexity

Conditions 9
Paths 18

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 25
nc 18
nop 0
dl 0
loc 39
rs 4.909
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
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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