Completed
Branch master (19cd63)
by
unknown
40:04
created

EditPage::addEditNotices()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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