Completed
Branch master (9ca75b)
by
unknown
26:06
created

EditPage::addTalkPageText()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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