Completed
Branch master (9259dd)
by
unknown
27:26
created

EditPage   F

Complexity

Total Complexity 617

Size/Duplication

Total Lines 4209
Duplicated Lines 3.59 %

Coupling/Cohesion

Components 1
Dependencies 40

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 151
loc 4209
rs 0.5217
wmc 617
lcom 1
cbo 40

82 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getArticle() 0 3 1
A getTitle() 0 3 1
A setContextTitle() 0 3 1
A getContextTitle() 0 8 2
A isSupportedContentModel() 0 4 2
A setApiEditOverride() 0 3 1
A submit() 0 3 1
D edit() 0 112 23
C getEditPermissionErrors() 0 27 7
C displayPermissionsError() 0 25 7
B displayViewSourcePage() 0 49 5
D previewOnOpen() 0 26 10
A isWrongCaseCssJsPage() 0 13 3
A isSectionEditSupported() 0 4 1
F importFormData() 0 205 32
A importContentFormData() 0 3 1
D initialiseForm() 0 31 9
D getContentObject() 0 115 24
A getOriginalContent() 0 16 4
A getParentRevId() 0 7 2
B getCurrentContent() 0 19 5
A setPreloadedContent() 0 3 1
C getPreloadedContent() 8 56 13
A tokenOk() 0 7 1
A setPostEditCookie() 0 16 3
A attemptSave() 0 11 2
D handleStatus() 0 121 38
C runPostMergeFilters() 0 47 7
B newSectionSummary() 0 23 4
F internalAttemptSave() 30 452 83
A addContentModelChangeLogEntry() 0 13 2
A updateWatchlist() 0 18 3
B mergeChangesIntoContent() 0 33 6
A getBaseRevision() 0 9 3
A matchSpamRegex() 0 6 1
A matchSummarySpamRegex() 0 5 1
A matchSpamRegexInternal() 0 9 3
D setHeaders() 0 54 13
D showIntro() 28 108 22
B showCustomIntro() 0 15 5
B toEditText() 0 12 5
A toEditContent() 0 15 4
F showEditForm() 18 232 30
A extractSectionTitle() 0 9 2
F showHeader() 11 220 51
B getSummaryInput() 0 31 6
B showSummaryInput() 0 23 6
B getSummaryPreview() 0 20 6
A showFormBeforeText() 0 16 2
A showFormAfterText() 0 16 1
A showContentForm() 0 3 1
C showTextbox1() 0 41 11
A showTextbox2() 0 3 1
B showTextbox() 0 28 2
B displayPreviewArea() 13 41 6
A showPreview() 0 13 3
C showDiff() 0 59 12
A showHeaderCopyrightWarning() 0 8 2
A showTosSummary() 0 10 2
A showEditTools() 0 6 1
A getCopywarn() 0 3 1
A getCopyrightWarning() 0 16 2
C getPreviewLimitReport() 0 41 8
B showStandardInputs() 4 46 3
B showConflict() 0 32 4
A getCancelLink() 0 16 4
A getActionURL() 0 3 1
B wasDeletedSinceLastEdit() 0 19 6
B getLastDelete() 0 38 4
F getPreviewText() 0 151 28
B getTemplates() 0 16 6
C getEditToolbar() 0 128 8
B getCheckboxes() 39 55 6
B getEditButtons() 0 32 1
A noSuchSectionPage() 0 11 1
B spamPageWithContent() 0 24 3
A checkUnicodeCompliantBrowser() 0 16 4
A safeUnicodeInput() 0 6 2
A safeUnicodeOutput() 0 7 2
C makeSafe() 0 33 7
C unmakeSafe() 0 28 8

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EditPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EditPage, and based on these observations, apply Extract Interface, too.

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
/**
24
 * The edit page/HTML interface (split from Article)
25
 * The actual database and text munging is still in Article,
26
 * but it should get easier to call those from alternate
27
 * interfaces.
28
 *
29
 * EditPage cares about two distinct titles:
30
 * $this->mContextTitle is the page that forms submit to, links point to,
31
 * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
32
 * page in the database that is actually being edited. These are
33
 * usually the same, but they are now allowed to be different.
34
 *
35
 * Surgeon General's Warning: prolonged exposure to this class is known to cause
36
 * headaches, which may be fatal.
37
 */
38
class EditPage {
39
	/**
40
	 * Status: Article successfully updated
41
	 */
42
	const AS_SUCCESS_UPDATE = 200;
43
44
	/**
45
	 * Status: Article successfully created
46
	 */
47
	const AS_SUCCESS_NEW_ARTICLE = 201;
48
49
	/**
50
	 * Status: Article update aborted by a hook function
51
	 */
52
	const AS_HOOK_ERROR = 210;
53
54
	/**
55
	 * Status: A hook function returned an error
56
	 */
57
	const AS_HOOK_ERROR_EXPECTED = 212;
58
59
	/**
60
	 * Status: User is blocked from editing this page
61
	 */
62
	const AS_BLOCKED_PAGE_FOR_USER = 215;
63
64
	/**
65
	 * Status: Content too big (> $wgMaxArticleSize)
66
	 */
67
	const AS_CONTENT_TOO_BIG = 216;
68
69
	/**
70
	 * Status: this anonymous user is not allowed to edit this page
71
	 */
72
	const AS_READ_ONLY_PAGE_ANON = 218;
73
74
	/**
75
	 * Status: this logged in user is not allowed to edit this page
76
	 */
77
	const AS_READ_ONLY_PAGE_LOGGED = 219;
78
79
	/**
80
	 * Status: wiki is in readonly mode (wfReadOnly() == true)
81
	 */
82
	const AS_READ_ONLY_PAGE = 220;
83
84
	/**
85
	 * Status: rate limiter for action 'edit' was tripped
86
	 */
87
	const AS_RATE_LIMITED = 221;
88
89
	/**
90
	 * Status: article was deleted while editing and param wpRecreate == false or form
91
	 * was not posted
92
	 */
93
	const AS_ARTICLE_WAS_DELETED = 222;
94
95
	/**
96
	 * Status: user tried to create this page, but is not allowed to do that
97
	 * ( Title->userCan('create') == false )
98
	 */
99
	const AS_NO_CREATE_PERMISSION = 223;
100
101
	/**
102
	 * Status: user tried to create a blank page and wpIgnoreBlankArticle == false
103
	 */
104
	const AS_BLANK_ARTICLE = 224;
105
106
	/**
107
	 * Status: (non-resolvable) edit conflict
108
	 */
109
	const AS_CONFLICT_DETECTED = 225;
110
111
	/**
112
	 * Status: no edit summary given and the user has forceeditsummary set and the user is not
113
	 * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false
114
	 */
115
	const AS_SUMMARY_NEEDED = 226;
116
117
	/**
118
	 * Status: user tried to create a new section without content
119
	 */
120
	const AS_TEXTBOX_EMPTY = 228;
121
122
	/**
123
	 * Status: article is too big (> $wgMaxArticleSize), after merging in the new section
124
	 */
125
	const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
126
127
	/**
128
	 * Status: WikiPage::doEdit() was unsuccessful
129
	 */
130
	const AS_END = 231;
131
132
	/**
133
	 * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex
134
	 */
135
	const AS_SPAM_ERROR = 232;
136
137
	/**
138
	 * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
139
	 */
140
	const AS_IMAGE_REDIRECT_ANON = 233;
141
142
	/**
143
	 * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
144
	 */
145
	const AS_IMAGE_REDIRECT_LOGGED = 234;
146
147
	/**
148
	 * Status: user tried to modify the content model, but is not allowed to do that
149
	 * ( User::isAllowed('editcontentmodel') == false )
150
	 */
151
	const AS_NO_CHANGE_CONTENT_MODEL = 235;
152
153
	/**
154
	 * Status: user tried to create self-redirect (redirect to the same article) and
155
	 * wpIgnoreSelfRedirect == false
156
	 */
157
	const AS_SELF_REDIRECT = 236;
158
159
	/**
160
	 * Status: an error relating to change tagging. Look at the message key for
161
	 * more details
162
	 */
163
	const AS_CHANGE_TAG_ERROR = 237;
164
165
	/**
166
	 * Status: can't parse content
167
	 */
168
	const AS_PARSE_ERROR = 240;
169
170
	/**
171
	 * Status: when changing the content model is disallowed due to
172
	 * $wgContentHandlerUseDB being false
173
	 */
174
	const AS_CANNOT_USE_CUSTOM_MODEL = 241;
175
176
	/**
177
	 * HTML id and name for the beginning of the edit form.
178
	 */
179
	const EDITFORM_ID = 'editform';
180
181
	/**
182
	 * Prefix of key for cookie used to pass post-edit state.
183
	 * The revision id edited is added after this
184
	 */
185
	const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
186
187
	/**
188
	 * Duration of PostEdit cookie, in seconds.
189
	 * The cookie will be removed instantly if the JavaScript runs.
190
	 *
191
	 * Otherwise, though, we don't want the cookies to accumulate.
192
	 * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible
193
	 * limit of only 20 cookies per domain. This still applies at least to some
194
	 * versions of IE without full updates:
195
	 * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
196
	 *
197
	 * A value of 20 minutes should be enough to take into account slow loads and minor
198
	 * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
199
	 */
200
	const POST_EDIT_COOKIE_DURATION = 1200;
201
202
	/** @var Article */
203
	public $mArticle;
204
	/** @var WikiPage */
205
	private $page;
206
207
	/** @var Title */
208
	public $mTitle;
209
210
	/** @var null|Title */
211
	private $mContextTitle = null;
212
213
	/** @var string */
214
	public $action = 'submit';
215
216
	/** @var bool */
217
	public $isConflict = false;
218
219
	/** @var bool */
220
	public $isCssJsSubpage = false;
221
222
	/** @var bool */
223
	public $isCssSubpage = false;
224
225
	/** @var bool */
226
	public $isJsSubpage = false;
227
228
	/** @var bool */
229
	public $isWrongCaseCssJsPage = false;
230
231
	/** @var bool New page or new section */
232
	public $isNew = false;
233
234
	/** @var bool */
235
	public $deletedSinceEdit;
236
237
	/** @var string */
238
	public $formtype;
239
240
	/** @var bool */
241
	public $firsttime;
242
243
	/** @var bool|stdClass */
244
	public $lastDelete;
245
246
	/** @var bool */
247
	public $mTokenOk = false;
248
249
	/** @var bool */
250
	public $mTokenOkExceptSuffix = false;
251
252
	/** @var bool */
253
	public $mTriedSave = false;
254
255
	/** @var bool */
256
	public $incompleteForm = false;
257
258
	/** @var bool */
259
	public $tooBig = false;
260
261
	/** @var bool */
262
	public $kblength = false;
263
264
	/** @var bool */
265
	public $missingComment = false;
266
267
	/** @var bool */
268
	public $missingSummary = false;
269
270
	/** @var bool */
271
	public $allowBlankSummary = false;
272
273
	/** @var bool */
274
	protected $blankArticle = false;
275
276
	/** @var bool */
277
	protected $allowBlankArticle = false;
278
279
	/** @var bool */
280
	protected $selfRedirect = false;
281
282
	/** @var bool */
283
	protected $allowSelfRedirect = false;
284
285
	/** @var string */
286
	public $autoSumm = '';
287
288
	/** @var string */
289
	public $hookError = '';
290
291
	/** @var ParserOutput */
292
	public $mParserOutput;
293
294
	/** @var bool Has a summary been preset using GET parameter &summary= ? */
295
	public $hasPresetSummary = false;
296
297
	/** @var bool */
298
	public $mBaseRevision = false;
299
300
	/** @var bool */
301
	public $mShowSummaryField = true;
302
303
	# Form values
304
305
	/** @var bool */
306
	public $save = false;
307
308
	/** @var bool */
309
	public $preview = false;
310
311
	/** @var bool */
312
	public $diff = false;
313
314
	/** @var bool */
315
	public $minoredit = false;
316
317
	/** @var bool */
318
	public $watchthis = false;
319
320
	/** @var bool */
321
	public $recreate = false;
322
323
	/** @var string */
324
	public $textbox1 = '';
325
326
	/** @var string */
327
	public $textbox2 = '';
328
329
	/** @var string */
330
	public $summary = '';
331
332
	/** @var bool */
333
	public $nosummary = false;
334
335
	/** @var string */
336
	public $edittime = '';
337
338
	/** @var integer */
339
	private $editRevId = null;
340
341
	/** @var string */
342
	public $section = '';
343
344
	/** @var string */
345
	public $sectiontitle = '';
346
347
	/** @var string */
348
	public $starttime = '';
349
350
	/** @var int */
351
	public $oldid = 0;
352
353
	/** @var int */
354
	public $parentRevId = 0;
355
356
	/** @var string */
357
	public $editintro = '';
358
359
	/** @var null */
360
	public $scrolltop = null;
361
362
	/** @var bool */
363
	public $bot = true;
364
365
	/** @var null|string */
366
	public $contentModel = null;
367
368
	/** @var null|string */
369
	public $contentFormat = null;
370
371
	/** @var null|array */
372
	private $changeTags = null;
373
374
	# Placeholders for text injection by hooks (must be HTML)
375
	# extensions should take care to _append_ to the present value
376
377
	/** @var string Before even the preview */
378
	public $editFormPageTop = '';
379
	public $editFormTextTop = '';
380
	public $editFormTextBeforeContent = '';
381
	public $editFormTextAfterWarn = '';
382
	public $editFormTextAfterTools = '';
383
	public $editFormTextBottom = '';
384
	public $editFormTextAfterContent = '';
385
	public $previewTextAfterContent = '';
386
	public $mPreloadContent = null;
387
388
	/* $didSave should be set to true whenever an article was successfully altered. */
389
	public $didSave = false;
390
	public $undidRev = 0;
391
392
	public $suppressIntro = false;
393
394
	/** @var bool */
395
	protected $edit;
396
397
	/**
398
	 * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
399
	 */
400
	private $enableApiEditOverride = false;
401
402
	/**
403
	 * @param Article $article
404
	 */
405
	public function __construct( Article $article ) {
406
		$this->mArticle = $article;
407
		$this->page = $article->getPage(); // model object
408
		$this->mTitle = $article->getTitle();
409
410
		$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...
411
412
		$handler = ContentHandler::getForModelID( $this->contentModel );
413
		$this->contentFormat = $handler->getDefaultFormat();
414
	}
415
416
	/**
417
	 * @return Article
418
	 */
419
	public function getArticle() {
420
		return $this->mArticle;
421
	}
422
423
	/**
424
	 * @since 1.19
425
	 * @return Title
426
	 */
427
	public function getTitle() {
428
		return $this->mTitle;
429
	}
430
431
	/**
432
	 * Set the context Title object
433
	 *
434
	 * @param Title|null $title Title object or null
435
	 */
436
	public function setContextTitle( $title ) {
437
		$this->mContextTitle = $title;
438
	}
439
440
	/**
441
	 * Get the context title object.
442
	 * If not set, $wgTitle will be returned. This behavior might change in
443
	 * the future to return $this->mTitle instead.
444
	 *
445
	 * @return Title
446
	 */
447
	public function getContextTitle() {
448
		if ( is_null( $this->mContextTitle ) ) {
449
			global $wgTitle;
450
			return $wgTitle;
451
		} else {
452
			return $this->mContextTitle;
453
		}
454
	}
455
456
	/**
457
	 * Returns if the given content model is editable.
458
	 *
459
	 * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
460
	 * @return bool
461
	 * @throws MWException If $modelId has no known handler
462
	 */
463
	public function isSupportedContentModel( $modelId ) {
464
		return $this->enableApiEditOverride === true ||
465
			ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
466
	}
467
468
	/**
469
	 * Allow editing of content that supports API direct editing, but not general
470
	 * direct editing. Set to false by default.
471
	 *
472
	 * @param bool $enableOverride
473
	 */
474
	public function setApiEditOverride( $enableOverride ) {
475
		$this->enableApiEditOverride = $enableOverride;
476
	}
477
478
	function submit() {
479
		$this->edit();
480
	}
481
482
	/**
483
	 * This is the function that gets called for "action=edit". It
484
	 * sets up various member variables, then passes execution to
485
	 * another function, usually showEditForm()
486
	 *
487
	 * The edit form is self-submitting, so that when things like
488
	 * preview and edit conflicts occur, we get the same form back
489
	 * with the extra stuff added.  Only when the final submission
490
	 * is made and all is well do we actually save and redirect to
491
	 * the newly-edited page.
492
	 */
493
	function edit() {
494
		global $wgOut, $wgRequest, $wgUser;
495
		// Allow extensions to modify/prevent this form or submission
496
		if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
497
			return;
498
		}
499
500
		wfDebug( __METHOD__ . ": enter\n" );
501
502
		// If they used redlink=1 and the page exists, redirect to the main article
503
		if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
504
			$wgOut->redirect( $this->mTitle->getFullURL() );
505
			return;
506
		}
507
508
		$this->importFormData( $wgRequest );
509
		$this->firsttime = false;
510
511
		if ( wfReadOnly() && $this->save ) {
512
			// Force preview
513
			$this->save = false;
514
			$this->preview = true;
515
		}
516
517
		if ( $this->save ) {
518
			$this->formtype = 'save';
519
		} elseif ( $this->preview ) {
520
			$this->formtype = 'preview';
521
		} elseif ( $this->diff ) {
522
			$this->formtype = 'diff';
523
		} else { # First time through
524
			$this->firsttime = true;
525
			if ( $this->previewOnOpen() ) {
526
				$this->formtype = 'preview';
527
			} else {
528
				$this->formtype = 'initial';
529
			}
530
		}
531
532
		$permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
533
		if ( $permErrors ) {
534
			wfDebug( __METHOD__ . ": User can't edit\n" );
535
			// Auto-block user's IP if the account was "hard" blocked
536
			if ( !wfReadOnly() ) {
537
				$user = $wgUser;
538
				DeferredUpdates::addCallableUpdate( function () use ( $user ) {
539
					$user->spreadAnyEditBlock();
540
				} );
541
			}
542
			$this->displayPermissionsError( $permErrors );
543
544
			return;
545
		}
546
547
		$revision = $this->mArticle->getRevisionFetched();
548
		// Disallow editing revisions with content models different from the current one
549
		if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
550
			$this->displayViewSourcePage(
551
				$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...
552
				wfMessage(
553
					'contentmodelediterror',
554
					$revision->getContentModel(),
555
					$this->contentModel
556
				)->plain()
557
			);
558
			return;
559
		}
560
561
		$this->isConflict = false;
562
		// css / js subpages of user pages get a special treatment
563
		$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
564
		$this->isCssSubpage = $this->mTitle->isCssSubpage();
565
		$this->isJsSubpage = $this->mTitle->isJsSubpage();
566
		// @todo FIXME: Silly assignment.
567
		$this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
568
569
		# Show applicable editing introductions
570
		if ( $this->formtype == 'initial' || $this->firsttime ) {
571
			$this->showIntro();
572
		}
573
574
		# Attempt submission here.  This will check for edit conflicts,
575
		# and redundantly check for locked database, blocked IPs, etc.
576
		# that edit() already checked just in case someone tries to sneak
577
		# in the back door with a hand-edited submission URL.
578
579
		if ( 'save' == $this->formtype ) {
580
			$resultDetails = null;
581
			$status = $this->attemptSave( $resultDetails );
582
			if ( !$this->handleStatus( $status, $resultDetails ) ) {
583
				return;
584
			}
585
		}
586
587
		# First time through: get contents, set time for conflict
588
		# checking, etc.
589
		if ( 'initial' == $this->formtype || $this->firsttime ) {
590
			if ( $this->initialiseForm() === false ) {
591
				$this->noSuchSectionPage();
592
				return;
593
			}
594
595
			if ( !$this->mTitle->getArticleID() ) {
596
				Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
597
			} else {
598
				Hooks::run( 'EditFormInitialText', [ $this ] );
599
			}
600
601
		}
602
603
		$this->showEditForm();
604
	}
605
606
	/**
607
	 * @param string $rigor Same format as Title::getUserPermissionErrors()
608
	 * @return array
609
	 */
610
	protected function getEditPermissionErrors( $rigor = 'secure' ) {
611
		global $wgUser;
612
613
		$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
614
		# Can this title be created?
615
		if ( !$this->mTitle->exists() ) {
616
			$permErrors = array_merge(
617
				$permErrors,
618
				wfArrayDiff2(
619
					$this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
620
					$permErrors
621
				)
622
			);
623
		}
624
		# Ignore some permissions errors when a user is just previewing/viewing diffs
625
		$remove = [];
626
		foreach ( $permErrors as $error ) {
627
			if ( ( $this->preview || $this->diff )
628
				&& ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
629
			) {
630
				$remove[] = $error;
631
			}
632
		}
633
		$permErrors = wfArrayDiff2( $permErrors, $remove );
634
635
		return $permErrors;
636
	}
637
638
	/**
639
	 * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
640
	 * but with the following differences:
641
	 * - If redlink=1, the user will be redirected to the page
642
	 * - If there is content to display or the error occurs while either saving,
643
	 *   previewing or showing the difference, it will be a
644
	 *   "View source for ..." page displaying the source code after the error message.
645
	 *
646
	 * @since 1.19
647
	 * @param array $permErrors Array of permissions errors, as returned by
648
	 *    Title::getUserPermissionsErrors().
649
	 * @throws PermissionsError
650
	 */
651
	protected function displayPermissionsError( array $permErrors ) {
652
		global $wgRequest, $wgOut;
653
654
		if ( $wgRequest->getBool( 'redlink' ) ) {
655
			// The edit page was reached via a red link.
656
			// Redirect to the article page and let them click the edit tab if
657
			// they really want a permission error.
658
			$wgOut->redirect( $this->mTitle->getFullURL() );
659
			return;
660
		}
661
662
		$content = $this->getContentObject();
663
664
		# Use the normal message if there's nothing to display
665
		if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
666
			$action = $this->mTitle->exists() ? 'edit' :
667
				( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
668
			throw new PermissionsError( $action, $permErrors );
669
		}
670
671
		$this->displayViewSourcePage(
672
			$content,
0 ignored issues
show
Bug introduced by
It seems like $content defined by $this->getContentObject() on line 662 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...
673
			$wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
674
		);
675
	}
676
677
	/**
678
	 * Display a read-only View Source page
679
	 * @param Content $content content object
680
	 * @param string $errorMessage additional wikitext error message to display
681
	 */
682
	protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
683
		global $wgOut;
684
685
		Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
686
687
		$wgOut->setRobotPolicy( 'noindex,nofollow' );
688
		$wgOut->setPageTitle( wfMessage(
689
			'viewsource-title',
690
			$this->getContextTitle()->getPrefixedText()
691
		) );
692
		$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
693
		$wgOut->addHTML( $this->editFormPageTop );
694
		$wgOut->addHTML( $this->editFormTextTop );
695
696
		if ( $errorMessage !== '' ) {
697
			$wgOut->addWikiText( $errorMessage );
698
			$wgOut->addHTML( "<hr />\n" );
699
		}
700
701
		# If the user made changes, preserve them when showing the markup
702
		# (This happens when a user is blocked during edit, for instance)
703
		if ( !$this->firsttime ) {
704
			$text = $this->textbox1;
705
			$wgOut->addWikiMsg( 'viewyourtext' );
706
		} else {
707
			try {
708
				$text = $this->toEditText( $content );
709
			} catch ( MWException $e ) {
710
				# Serialize using the default format if the content model is not supported
711
				# (e.g. for an old revision with a different model)
712
				$text = $content->serialize();
713
			}
714
			$wgOut->addWikiMsg( 'viewsourcetext' );
715
		}
716
717
		$wgOut->addHTML( $this->editFormTextBeforeContent );
718
		$this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
719
		$wgOut->addHTML( $this->editFormTextAfterContent );
720
721
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
722
			Linker::formatTemplates( $this->getTemplates() ) ) );
723
724
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
725
726
		$wgOut->addHTML( $this->editFormTextBottom );
727
		if ( $this->mTitle->exists() ) {
728
			$wgOut->returnToMain( null, $this->mTitle );
729
		}
730
	}
731
732
	/**
733
	 * Should we show a preview when the edit form is first shown?
734
	 *
735
	 * @return bool
736
	 */
737
	protected function previewOnOpen() {
738
		global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
739
		if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
740
			// Explicit override from request
741
			return true;
742
		} elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
743
			// Explicit override from request
744
			return false;
745
		} elseif ( $this->section == 'new' ) {
746
			// Nothing *to* preview for new sections
747
			return false;
748
		} elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
749
			&& $wgUser->getOption( 'previewonfirst' )
750
		) {
751
			// Standard preference behavior
752
			return true;
753
		} elseif ( !$this->mTitle->exists()
754
			&& isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
755
			&& $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
756
		) {
757
			// Categories are special
758
			return true;
759
		} else {
760
			return false;
761
		}
762
	}
763
764
	/**
765
	 * Checks whether the user entered a skin name in uppercase,
766
	 * e.g. "User:Example/Monobook.css" instead of "monobook.css"
767
	 *
768
	 * @return bool
769
	 */
770
	protected function isWrongCaseCssJsPage() {
771
		if ( $this->mTitle->isCssJsSubpage() ) {
772
			$name = $this->mTitle->getSkinFromCssJsSubpage();
773
			$skins = array_merge(
774
				array_keys( Skin::getSkinNames() ),
775
				[ 'common' ]
776
			);
777
			return !in_array( $name, $skins )
778
				&& in_array( strtolower( $name ), $skins );
779
		} else {
780
			return false;
781
		}
782
	}
783
784
	/**
785
	 * Returns whether section editing is supported for the current page.
786
	 * Subclasses may override this to replace the default behavior, which is
787
	 * to check ContentHandler::supportsSections.
788
	 *
789
	 * @return bool True if this edit page supports sections, false otherwise.
790
	 */
791
	protected function isSectionEditSupported() {
792
		$contentHandler = ContentHandler::getForTitle( $this->mTitle );
793
		return $contentHandler->supportsSections();
794
	}
795
796
	/**
797
	 * This function collects the form data and uses it to populate various member variables.
798
	 * @param WebRequest $request
799
	 * @throws ErrorPageError
800
	 */
801
	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...
802
		global $wgContLang, $wgUser;
803
804
		# Section edit can come from either the form or a link
805
		$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
806
807
		if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
808
			throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
809
		}
810
811
		$this->isNew = !$this->mTitle->exists() || $this->section == 'new';
812
813
		if ( $request->wasPosted() ) {
814
			# These fields need to be checked for encoding.
815
			# Also remove trailing whitespace, but don't remove _initial_
816
			# whitespace from the text boxes. This may be significant formatting.
817
			$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
818
			if ( !$request->getCheck( 'wpTextbox2' ) ) {
819
				// Skip this if wpTextbox2 has input, it indicates that we came
820
				// from a conflict page with raw page text, not a custom form
821
				// modified by subclasses
822
				$textbox1 = $this->importContentFormData( $request );
823
				if ( $textbox1 !== null ) {
824
					$this->textbox1 = $textbox1;
825
				}
826
			}
827
828
			# Truncate for whole multibyte characters
829
			$this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
830
831
			# If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
832
			# header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
833
			# section titles.
834
			$this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
835
836
			# Treat sectiontitle the same way as summary.
837
			# Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
838
			# currently doing double duty as both edit summary and section title. Right now this
839
			# is just to allow API edits to work around this limitation, but this should be
840
			# incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
841
			$this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
842
			$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
843
844
			$this->edittime = $request->getVal( 'wpEdittime' );
845
			$this->editRevId = $request->getIntOrNull( 'editRevId' );
846
			$this->starttime = $request->getVal( 'wpStarttime' );
847
848
			$undidRev = $request->getInt( 'wpUndidRevision' );
849
			if ( $undidRev ) {
850
				$this->undidRev = $undidRev;
851
			}
852
853
			$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...
854
855
			if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
856
				// wpTextbox1 field is missing, possibly due to being "too big"
857
				// according to some filter rules such as Suhosin's setting for
858
				// suhosin.request.max_value_length (d'oh)
859
				$this->incompleteForm = true;
860
			} else {
861
				// If we receive the last parameter of the request, we can fairly
862
				// claim the POST request has not been truncated.
863
864
				// TODO: softened the check for cutover.  Once we determine
865
				// that it is safe, we should complete the transition by
866
				// removing the "edittime" clause.
867
				$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...
868
					&& is_null( $this->edittime ) );
869
			}
870
			if ( $this->incompleteForm ) {
871
				# If the form is incomplete, force to preview.
872
				wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
873
				wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
874
				$this->preview = true;
875
			} else {
876
				$this->preview = $request->getCheck( 'wpPreview' );
877
				$this->diff = $request->getCheck( 'wpDiff' );
878
879
				// Remember whether a save was requested, so we can indicate
880
				// if we forced preview due to session failure.
881
				$this->mTriedSave = !$this->preview;
882
883
				if ( $this->tokenOk( $request ) ) {
884
					# Some browsers will not report any submit button
885
					# if the user hits enter in the comment box.
886
					# The unmarked state will be assumed to be a save,
887
					# if the form seems otherwise complete.
888
					wfDebug( __METHOD__ . ": Passed token check.\n" );
889
				} elseif ( $this->diff ) {
890
					# Failed token check, but only requested "Show Changes".
891
					wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
892
				} else {
893
					# Page might be a hack attempt posted from
894
					# an external site. Preview instead of saving.
895
					wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
896
					$this->preview = true;
897
				}
898
			}
899
			$this->save = !$this->preview && !$this->diff;
900
			if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
901
				$this->edittime = null;
902
			}
903
904
			if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
905
				$this->starttime = null;
906
			}
907
908
			$this->recreate = $request->getCheck( 'wpRecreate' );
909
910
			$this->minoredit = $request->getCheck( 'wpMinoredit' );
911
			$this->watchthis = $request->getCheck( 'wpWatchthis' );
912
913
			# Don't force edit summaries when a user is editing their own user or talk page
914
			if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
915
				&& $this->mTitle->getText() == $wgUser->getName()
916
			) {
917
				$this->allowBlankSummary = true;
918
			} else {
919
				$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
920
					|| !$wgUser->getOption( 'forceeditsummary' );
921
			}
922
923
			$this->autoSumm = $request->getText( 'wpAutoSummary' );
924
925
			$this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
926
			$this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
927
928
			$changeTags = $request->getVal( 'wpChangeTags' );
929
			if ( is_null( $changeTags ) || $changeTags === '' ) {
930
				$this->changeTags = [];
931
			} else {
932
				$this->changeTags = array_filter( array_map( 'trim', explode( ',',
933
					$changeTags ) ) );
934
			}
935
		} else {
936
			# Not a posted form? Start with nothing.
937
			wfDebug( __METHOD__ . ": Not a posted form.\n" );
938
			$this->textbox1 = '';
939
			$this->summary = '';
940
			$this->sectiontitle = '';
941
			$this->edittime = '';
942
			$this->editRevId = null;
943
			$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...
944
			$this->edit = false;
945
			$this->preview = false;
946
			$this->save = false;
947
			$this->diff = false;
948
			$this->minoredit = false;
949
			// Watch may be overridden by request parameters
950
			$this->watchthis = $request->getBool( 'watchthis', false );
951
			$this->recreate = false;
952
953
			// When creating a new section, we can preload a section title by passing it as the
954
			// preloadtitle parameter in the URL (Bug 13100)
955
			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...
956
				$this->sectiontitle = $request->getVal( 'preloadtitle' );
957
				// Once wpSummary isn't being use for setting section titles, we should delete this.
958
				$this->summary = $request->getVal( 'preloadtitle' );
959
			} 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...
960
				$this->summary = $request->getText( 'summary' );
961
				if ( $this->summary !== '' ) {
962
					$this->hasPresetSummary = true;
963
				}
964
			}
965
966
			if ( $request->getVal( 'minor' ) ) {
967
				$this->minoredit = true;
968
			}
969
		}
970
971
		$this->oldid = $request->getInt( 'oldid' );
972
		$this->parentRevId = $request->getInt( 'parentRevId' );
973
974
		$this->bot = $request->getBool( 'bot', true );
975
		$this->nosummary = $request->getBool( 'nosummary' );
976
977
		// May be overridden by revision.
978
		$this->contentModel = $request->getText( 'model', $this->contentModel );
979
		// May be overridden by revision.
980
		$this->contentFormat = $request->getText( 'format', $this->contentFormat );
981
982
		if ( !ContentHandler::getForModelID( $this->contentModel )
983
			->isSupportedFormat( $this->contentFormat )
984
		) {
985
			throw new ErrorPageError(
986
				'editpage-notsupportedcontentformat-title',
987
				'editpage-notsupportedcontentformat-text',
988
				[ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ]
989
			);
990
		}
991
992
		/**
993
		 * @todo Check if the desired model is allowed in this namespace, and if
994
		 *   a transition from the page's current model to the new model is
995
		 *   allowed.
996
		 */
997
998
		$this->editintro = $request->getText( 'editintro',
999
			// Custom edit intro for new sections
1000
			$this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1001
1002
		// Allow extensions to modify form data
1003
		Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1004
1005
	}
1006
1007
	/**
1008
	 * Subpage overridable method for extracting the page content data from the
1009
	 * posted form to be placed in $this->textbox1, if using customized input
1010
	 * this method should be overridden and return the page text that will be used
1011
	 * for saving, preview parsing and so on...
1012
	 *
1013
	 * @param WebRequest $request
1014
	 * @return string|null
1015
	 */
1016
	protected function importContentFormData( &$request ) {
1017
		return; // Don't do anything, EditPage already extracted wpTextbox1
1018
	}
1019
1020
	/**
1021
	 * Initialise form fields in the object
1022
	 * Called on the first invocation, e.g. when a user clicks an edit link
1023
	 * @return bool If the requested section is valid
1024
	 */
1025
	function initialiseForm() {
1026
		global $wgUser;
1027
		$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...
1028
		$this->editRevId = $this->page->getLatest();
1029
1030
		$content = $this->getContentObject( false ); # TODO: track content object?!
1031
		if ( $content === false ) {
1032
			return false;
1033
		}
1034
		$this->textbox1 = $this->toEditText( $content );
1035
1036
		// activate checkboxes if user wants them to be always active
1037
		# Sort out the "watch" checkbox
1038
		if ( $wgUser->getOption( 'watchdefault' ) ) {
1039
			# Watch all edits
1040
			$this->watchthis = true;
1041
		} elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1042
			# Watch creations
1043
			$this->watchthis = true;
1044
		} elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1045
			# Already watched
1046
			$this->watchthis = true;
1047
		}
1048
		if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1049
			$this->minoredit = true;
1050
		}
1051
		if ( $this->textbox1 === false ) {
1052
			return false;
1053
		}
1054
		return true;
1055
	}
1056
1057
	/**
1058
	 * @param Content|null $def_content The default value to return
1059
	 *
1060
	 * @return Content|null Content on success, $def_content for invalid sections
1061
	 *
1062
	 * @since 1.21
1063
	 */
1064
	protected function getContentObject( $def_content = null ) {
1065
		global $wgOut, $wgRequest, $wgUser, $wgContLang;
1066
1067
		$content = false;
1068
1069
		// For message page not locally set, use the i18n message.
1070
		// For other non-existent articles, use preload text if any.
1071
		if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1072
			if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1073
				# If this is a system message, get the default text.
1074
				$msg = $this->mTitle->getDefaultMessageText();
1075
1076
				$content = $this->toEditContent( $msg );
1077
			}
1078
			if ( $content === false ) {
1079
				# If requested, preload some text.
1080
				$preload = $wgRequest->getVal( 'preload',
1081
					// Custom preload text for new sections
1082
					$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1083
				$params = $wgRequest->getArray( 'preloadparams', [] );
1084
1085
				$content = $this->getPreloadedContent( $preload, $params );
1086
			}
1087
		// For existing pages, get text based on "undo" or section parameters.
1088
		} else {
1089
			if ( $this->section != '' ) {
1090
				// Get section edit text (returns $def_text for invalid sections)
1091
				$orig = $this->getOriginalContent( $wgUser );
1092
				$content = $orig ? $orig->getSection( $this->section ) : null;
1093
1094
				if ( !$content ) {
1095
					$content = $def_content;
1096
				}
1097
			} else {
1098
				$undoafter = $wgRequest->getInt( 'undoafter' );
1099
				$undo = $wgRequest->getInt( 'undo' );
1100
1101
				if ( $undo > 0 && $undoafter > 0 ) {
1102
					$undorev = Revision::newFromId( $undo );
1103
					$oldrev = Revision::newFromId( $undoafter );
1104
1105
					# Sanity check, make sure it's the right page,
1106
					# the revisions exist and they were not deleted.
1107
					# Otherwise, $content will be left as-is.
1108
					if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1109
						!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1110
						!$oldrev->isDeleted( Revision::DELETED_TEXT )
1111
					) {
1112
						$content = $this->page->getUndoContent( $undorev, $oldrev );
1113
1114
						if ( $content === false ) {
1115
							# Warn the user that something went wrong
1116
							$undoMsg = 'failure';
1117
						} else {
1118
							$oldContent = $this->page->getContent( Revision::RAW );
1119
							$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1120
							$newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1121
1122
							if ( $newContent->equals( $oldContent ) ) {
1123
								# Tell the user that the undo results in no change,
1124
								# i.e. the revisions were already undone.
1125
								$undoMsg = 'nochange';
1126
								$content = false;
1127
							} else {
1128
								# Inform the user of our success and set an automatic edit summary
1129
								$undoMsg = 'success';
1130
1131
								# If we just undid one rev, use an autosummary
1132
								$firstrev = $oldrev->getNext();
1133
								if ( $firstrev && $firstrev->getId() == $undo ) {
1134
									$userText = $undorev->getUserText();
1135
									if ( $userText === '' ) {
1136
										$undoSummary = wfMessage(
1137
											'undo-summary-username-hidden',
1138
											$undo
1139
										)->inContentLanguage()->text();
1140
									} else {
1141
										$undoSummary = wfMessage(
1142
											'undo-summary',
1143
											$undo,
1144
											$userText
1145
										)->inContentLanguage()->text();
1146
									}
1147
									if ( $this->summary === '' ) {
1148
										$this->summary = $undoSummary;
1149
									} else {
1150
										$this->summary = $undoSummary . wfMessage( 'colon-separator' )
1151
											->inContentLanguage()->text() . $this->summary;
1152
									}
1153
									$this->undidRev = $undo;
1154
								}
1155
								$this->formtype = 'diff';
1156
							}
1157
						}
1158
					} else {
1159
						// Failed basic sanity checks.
1160
						// Older revisions may have been removed since the link
1161
						// was created, or we may simply have got bogus input.
1162
						$undoMsg = 'norev';
1163
					}
1164
1165
					// Messages: undo-success, undo-failure, undo-norev, undo-nochange
1166
					$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1167
					$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1168
						wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1169
				}
1170
1171
				if ( $content === false ) {
1172
					$content = $this->getOriginalContent( $wgUser );
1173
				}
1174
			}
1175
		}
1176
1177
		return $content;
1178
	}
1179
1180
	/**
1181
	 * Get the content of the wanted revision, without section extraction.
1182
	 *
1183
	 * The result of this function can be used to compare user's input with
1184
	 * section replaced in its context (using WikiPage::replaceSectionAtRev())
1185
	 * to the original text of the edit.
1186
	 *
1187
	 * This differs from Article::getContent() that when a missing revision is
1188
	 * encountered the result will be null and not the
1189
	 * 'missing-revision' message.
1190
	 *
1191
	 * @since 1.19
1192
	 * @param User $user The user to get the revision for
1193
	 * @return Content|null
1194
	 */
1195
	private function getOriginalContent( User $user ) {
1196
		if ( $this->section == 'new' ) {
1197
			return $this->getCurrentContent();
1198
		}
1199
		$revision = $this->mArticle->getRevisionFetched();
1200
		if ( $revision === null ) {
1201
			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...
1202
				$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...
1203
			}
1204
			$handler = ContentHandler::getForModelID( $this->contentModel );
1205
1206
			return $handler->makeEmptyContent();
1207
		}
1208
		$content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1209
		return $content;
1210
	}
1211
1212
	/**
1213
	 * Get the edit's parent revision ID
1214
	 *
1215
	 * The "parent" revision is the ancestor that should be recorded in this
1216
	 * page's revision history.  It is either the revision ID of the in-memory
1217
	 * article content, or in the case of a 3-way merge in order to rebase
1218
	 * across a recoverable edit conflict, the ID of the newer revision to
1219
	 * which we have rebased this page.
1220
	 *
1221
	 * @since 1.27
1222
	 * @return int Revision ID
1223
	 */
1224
	public function getParentRevId() {
1225
		if ( $this->parentRevId ) {
1226
			return $this->parentRevId;
1227
		} else {
1228
			return $this->mArticle->getRevIdFetched();
1229
		}
1230
	}
1231
1232
	/**
1233
	 * Get the current content of the page. This is basically similar to
1234
	 * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
1235
	 * content object is returned instead of null.
1236
	 *
1237
	 * @since 1.21
1238
	 * @return Content
1239
	 */
1240
	protected function getCurrentContent() {
1241
		$rev = $this->page->getRevision();
1242
		$content = $rev ? $rev->getContent( Revision::RAW ) : null;
1243
1244
		if ( $content === false || $content === null ) {
1245
			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...
1246
				$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...
1247
			}
1248
			$handler = ContentHandler::getForModelID( $this->contentModel );
1249
1250
			return $handler->makeEmptyContent();
1251
		} else {
1252
			# nasty side-effect, but needed for consistency
1253
			$this->contentModel = $rev->getContentModel();
1254
			$this->contentFormat = $rev->getContentFormat();
1255
1256
			return $content;
1257
		}
1258
	}
1259
1260
	/**
1261
	 * Use this method before edit() to preload some content into the edit box
1262
	 *
1263
	 * @param Content $content
1264
	 *
1265
	 * @since 1.21
1266
	 */
1267
	public function setPreloadedContent( Content $content ) {
1268
		$this->mPreloadContent = $content;
1269
	}
1270
1271
	/**
1272
	 * Get the contents to be preloaded into the box, either set by
1273
	 * an earlier setPreloadText() or by loading the given page.
1274
	 *
1275
	 * @param string $preload Representing the title to preload from.
1276
	 * @param array $params Parameters to use (interface-message style) in the preloaded text
1277
	 *
1278
	 * @return Content
1279
	 *
1280
	 * @since 1.21
1281
	 */
1282
	protected function getPreloadedContent( $preload, $params = [] ) {
1283
		global $wgUser;
1284
1285
		if ( !empty( $this->mPreloadContent ) ) {
1286
			return $this->mPreloadContent;
1287
		}
1288
1289
		$handler = ContentHandler::getForTitle( $this->getTitle() );
1290
1291
		if ( $preload === '' ) {
1292
			return $handler->makeEmptyContent();
1293
		}
1294
1295
		$title = Title::newFromText( $preload );
1296
		# Check for existence to avoid getting MediaWiki:Noarticletext
1297 View Code Duplication
		if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1298
			// TODO: somehow show a warning to the user!
1299
			return $handler->makeEmptyContent();
1300
		}
1301
1302
		$page = WikiPage::factory( $title );
1303
		if ( $page->isRedirect() ) {
1304
			$title = $page->getRedirectTarget();
1305
			# Same as before
1306 View Code Duplication
			if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1307
				// TODO: somehow show a warning to the user!
1308
				return $handler->makeEmptyContent();
1309
			}
1310
			$page = WikiPage::factory( $title );
1311
		}
1312
1313
		$parserOptions = ParserOptions::newFromUser( $wgUser );
1314
		$content = $page->getContent( Revision::RAW );
1315
1316
		if ( !$content ) {
1317
			// TODO: somehow show a warning to the user!
1318
			return $handler->makeEmptyContent();
1319
		}
1320
1321
		if ( $content->getModel() !== $handler->getModelID() ) {
1322
			$converted = $content->convert( $handler->getModelID() );
1323
1324
			if ( !$converted ) {
1325
				// TODO: somehow show a warning to the user!
1326
				wfDebug( "Attempt to preload incompatible content: " .
1327
					"can't convert " . $content->getModel() .
1328
					" to " . $handler->getModelID() );
1329
1330
				return $handler->makeEmptyContent();
1331
			}
1332
1333
			$content = $converted;
1334
		}
1335
1336
		return $content->preloadTransform( $title, $parserOptions, $params );
1337
	}
1338
1339
	/**
1340
	 * Make sure the form isn't faking a user's credentials.
1341
	 *
1342
	 * @param WebRequest $request
1343
	 * @return bool
1344
	 * @private
1345
	 */
1346
	function tokenOk( &$request ) {
1347
		global $wgUser;
1348
		$token = $request->getVal( 'wpEditToken' );
1349
		$this->mTokenOk = $wgUser->matchEditToken( $token );
1350
		$this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1351
		return $this->mTokenOk;
1352
	}
1353
1354
	/**
1355
	 * Sets post-edit cookie indicating the user just saved a particular revision.
1356
	 *
1357
	 * This uses a temporary cookie for each revision ID so separate saves will never
1358
	 * interfere with each other.
1359
	 *
1360
	 * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
1361
	 * the redirect.  It must be clearable by JavaScript code, so it must not be
1362
	 * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
1363
	 * variable.
1364
	 *
1365
	 * If the variable were set on the server, it would be cached, which is unwanted
1366
	 * since the post-edit state should only apply to the load right after the save.
1367
	 *
1368
	 * @param int $statusValue The status value (to check for new article status)
1369
	 */
1370
	protected function setPostEditCookie( $statusValue ) {
1371
		$revisionId = $this->page->getLatest();
1372
		$postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1373
1374
		$val = 'saved';
1375
		if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1376
			$val = 'created';
1377
		} elseif ( $this->oldid ) {
1378
			$val = 'restored';
1379
		}
1380
1381
		$response = RequestContext::getMain()->getRequest()->response();
1382
		$response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1383
			'httpOnly' => false,
1384
		] );
1385
	}
1386
1387
	/**
1388
	 * Attempt submission
1389
	 * @param array $resultDetails See docs for $result in internalAttemptSave
1390
	 * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
1391
	 * @return Status The resulting status object.
1392
	 */
1393
	public function attemptSave( &$resultDetails = false ) {
1394
		global $wgUser;
1395
1396
		# Allow bots to exempt some edits from bot flagging
1397
		$bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1398
		$status = $this->internalAttemptSave( $resultDetails, $bot );
0 ignored issues
show
Bug introduced by
It seems like $resultDetails defined by parameter $resultDetails on line 1393 can also be of type false; 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...
1399
1400
		Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1401
1402
		return $status;
1403
	}
1404
1405
	/**
1406
	 * Handle status, such as after attempt save
1407
	 *
1408
	 * @param Status $status
1409
	 * @param array|bool $resultDetails
1410
	 *
1411
	 * @throws ErrorPageError
1412
	 * @return bool False, if output is done, true if rest of the form should be displayed
1413
	 */
1414
	private function handleStatus( Status $status, $resultDetails ) {
1415
		global $wgUser, $wgOut;
1416
1417
		/**
1418
		 * @todo FIXME: once the interface for internalAttemptSave() is made
1419
		 *   nicer, this should use the message in $status
1420
		 */
1421
		if ( $status->value == self::AS_SUCCESS_UPDATE
1422
			|| $status->value == self::AS_SUCCESS_NEW_ARTICLE
1423
		) {
1424
			$this->didSave = true;
1425
			if ( !$resultDetails['nullEdit'] ) {
1426
				$this->setPostEditCookie( $status->value );
1427
			}
1428
		}
1429
1430
		// "wpExtraQueryRedirect" is a hidden input to modify
1431
		// after save URL and is not used by actual edit form
1432
		$request = RequestContext::getMain()->getRequest();
1433
		$extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1434
1435
		switch ( $status->value ) {
1436
			case self::AS_HOOK_ERROR_EXPECTED:
1437
			case self::AS_CONTENT_TOO_BIG:
1438
			case self::AS_ARTICLE_WAS_DELETED:
1439
			case self::AS_CONFLICT_DETECTED:
1440
			case self::AS_SUMMARY_NEEDED:
1441
			case self::AS_TEXTBOX_EMPTY:
1442
			case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1443
			case self::AS_END:
1444
			case self::AS_BLANK_ARTICLE:
1445
			case self::AS_SELF_REDIRECT:
1446
				return true;
1447
1448
			case self::AS_HOOK_ERROR:
1449
				return false;
1450
1451
			case self::AS_CANNOT_USE_CUSTOM_MODEL:
1452
			case self::AS_PARSE_ERROR:
1453
				$wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1454
				return true;
1455
1456
			case self::AS_SUCCESS_NEW_ARTICLE:
1457
				$query = $resultDetails['redirect'] ? 'redirect=no' : '';
1458
				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...
1459
					if ( $query === '' ) {
1460
						$query = $extraQueryRedirect;
1461
					} else {
1462
						$query = $query . '&' . $extraQueryRedirect;
1463
					}
1464
				}
1465
				$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1466
				$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1467
				return false;
1468
1469
			case self::AS_SUCCESS_UPDATE:
1470
				$extraQuery = '';
1471
				$sectionanchor = $resultDetails['sectionanchor'];
1472
1473
				// Give extensions a chance to modify URL query on update
1474
				Hooks::run(
1475
					'ArticleUpdateBeforeRedirect',
1476
					[ $this->mArticle, &$sectionanchor, &$extraQuery ]
1477
				);
1478
1479
				if ( $resultDetails['redirect'] ) {
1480
					if ( $extraQuery == '' ) {
1481
						$extraQuery = 'redirect=no';
1482
					} else {
1483
						$extraQuery = 'redirect=no&' . $extraQuery;
1484
					}
1485
				}
1486
				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...
1487
					if ( $extraQuery === '' ) {
1488
						$extraQuery = $extraQueryRedirect;
1489
					} else {
1490
						$extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1491
					}
1492
				}
1493
1494
				$wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1495
				return false;
1496
1497
			case self::AS_SPAM_ERROR:
1498
				$this->spamPageWithContent( $resultDetails['spam'] );
1499
				return false;
1500
1501
			case self::AS_BLOCKED_PAGE_FOR_USER:
1502
				throw new UserBlockedError( $wgUser->getBlock() );
1503
1504
			case self::AS_IMAGE_REDIRECT_ANON:
1505
			case self::AS_IMAGE_REDIRECT_LOGGED:
1506
				throw new PermissionsError( 'upload' );
1507
1508
			case self::AS_READ_ONLY_PAGE_ANON:
1509
			case self::AS_READ_ONLY_PAGE_LOGGED:
1510
				throw new PermissionsError( 'edit' );
1511
1512
			case self::AS_READ_ONLY_PAGE:
1513
				throw new ReadOnlyError;
1514
1515
			case self::AS_RATE_LIMITED:
1516
				throw new ThrottledError();
1517
1518
			case self::AS_NO_CREATE_PERMISSION:
1519
				$permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1520
				throw new PermissionsError( $permission );
1521
1522
			case self::AS_NO_CHANGE_CONTENT_MODEL:
1523
				throw new PermissionsError( 'editcontentmodel' );
1524
1525
			default:
1526
				// We don't recognize $status->value. The only way that can happen
1527
				// is if an extension hook aborted from inside ArticleSave.
1528
				// Render the status object into $this->hookError
1529
				// FIXME this sucks, we should just use the Status object throughout
1530
				$this->hookError = '<div class="error">' . $status->getWikiText() .
1531
					'</div>';
1532
				return true;
1533
		}
1534
	}
1535
1536
	/**
1537
	 * Run hooks that can filter edits just before they get saved.
1538
	 *
1539
	 * @param Content $content The Content to filter.
1540
	 * @param Status $status For reporting the outcome to the caller
1541
	 * @param User $user The user performing the edit
1542
	 *
1543
	 * @return bool
1544
	 */
1545
	protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1546
		// Run old style post-section-merge edit filter
1547
		if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1548
			[ $this, $content, &$this->hookError, $this->summary ] )
1549
		) {
1550
			# Error messages etc. could be handled within the hook...
1551
			$status->fatal( 'hookaborted' );
1552
			$status->value = self::AS_HOOK_ERROR;
1553
			return false;
1554
		} elseif ( $this->hookError != '' ) {
1555
			# ...or the hook could be expecting us to produce an error
1556
			$status->fatal( 'hookaborted' );
1557
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1558
			return false;
1559
		}
1560
1561
		// Run new style post-section-merge edit filter
1562
		if ( !Hooks::run( 'EditFilterMergedContent',
1563
				[ $this->mArticle->getContext(), $content, $status, $this->summary,
1564
				$user, $this->minoredit ] )
1565
		) {
1566
			# Error messages etc. could be handled within the hook...
1567
			if ( $status->isGood() ) {
1568
				$status->fatal( 'hookaborted' );
1569
				// Not setting $this->hookError here is a hack to allow the hook
1570
				// to cause a return to the edit page without $this->hookError
1571
				// being set. This is used by ConfirmEdit to display a captcha
1572
				// without any error message cruft.
1573
			} else {
1574
				$this->hookError = $status->getWikiText();
1575
			}
1576
			// Use the existing $status->value if the hook set it
1577
			if ( !$status->value ) {
1578
				$status->value = self::AS_HOOK_ERROR;
1579
			}
1580
			return false;
1581
		} elseif ( !$status->isOK() ) {
1582
			# ...or the hook could be expecting us to produce an error
1583
			// FIXME this sucks, we should just use the Status object throughout
1584
			$this->hookError = $status->getWikiText();
1585
			$status->fatal( 'hookaborted' );
1586
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1587
			return false;
1588
		}
1589
1590
		return true;
1591
	}
1592
1593
	/**
1594
	 * Return the summary to be used for a new section.
1595
	 *
1596
	 * @param string $sectionanchor Set to the section anchor text
1597
	 * @return string
1598
	 */
1599
	private function newSectionSummary( &$sectionanchor = null ) {
1600
		global $wgParser;
1601
1602
		if ( $this->sectiontitle !== '' ) {
1603
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1604
			// If no edit summary was specified, create one automatically from the section
1605
			// title and have it link to the new section. Otherwise, respect the summary as
1606
			// passed.
1607
			if ( $this->summary === '' ) {
1608
				$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1609
				return wfMessage( 'newsectionsummary' )
1610
					->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1611
			}
1612
		} elseif ( $this->summary !== '' ) {
1613
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1614
			# This is a new section, so create a link to the new section
1615
			# in the revision summary.
1616
			$cleanSummary = $wgParser->stripSectionName( $this->summary );
1617
			return wfMessage( 'newsectionsummary' )
1618
				->rawParams( $cleanSummary )->inContentLanguage()->text();
1619
		}
1620
		return $this->summary;
1621
	}
1622
1623
	/**
1624
	 * Attempt submission (no UI)
1625
	 *
1626
	 * @param array $result Array to add statuses to, currently with the
1627
	 *   possible keys:
1628
	 *   - spam (string): Spam string from content if any spam is detected by
1629
	 *     matchSpamRegex.
1630
	 *   - sectionanchor (string): Section anchor for a section save.
1631
	 *   - nullEdit (boolean): Set if doEditContent is OK.  True if null edit,
1632
	 *     false otherwise.
1633
	 *   - redirect (bool): Set if doEditContent is OK. True if resulting
1634
	 *     revision is a redirect.
1635
	 * @param bool $bot True if edit is being made under the bot right.
1636
	 *
1637
	 * @return Status Status object, possibly with a message, but always with
1638
	 *   one of the AS_* constants in $status->value,
1639
	 *
1640
	 * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to
1641
	 *   various error display idiosyncrasies. There are also lots of cases
1642
	 *   where error metadata is set in the object and retrieved later instead
1643
	 *   of being returned, e.g. AS_CONTENT_TOO_BIG and
1644
	 *   AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some
1645
	 * time.
1646
	 */
1647
	function internalAttemptSave( &$result, $bot = false ) {
1648
		global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
1649
		global $wgContentHandlerUseDB;
1650
1651
		$status = Status::newGood();
1652
1653
		if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1654
			wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1655
			$status->fatal( 'hookaborted' );
1656
			$status->value = self::AS_HOOK_ERROR;
1657
			return $status;
1658
		}
1659
1660
		$spam = $wgRequest->getText( 'wpAntispam' );
1661
		if ( $spam !== '' ) {
1662
			wfDebugLog(
1663
				'SimpleAntiSpam',
1664
				$wgUser->getName() .
1665
				' editing "' .
1666
				$this->mTitle->getPrefixedText() .
1667
				'" submitted bogus field "' .
1668
				$spam .
1669
				'"'
1670
			);
1671
			$status->fatal( 'spamprotectionmatch', false );
1672
			$status->value = self::AS_SPAM_ERROR;
1673
			return $status;
1674
		}
1675
1676
		try {
1677
			# Construct Content object
1678
			$textbox_content = $this->toEditContent( $this->textbox1 );
1679
		} catch ( MWContentSerializationException $ex ) {
1680
			$status->fatal(
1681
				'content-failed-to-parse',
1682
				$this->contentModel,
1683
				$this->contentFormat,
1684
				$ex->getMessage()
1685
			);
1686
			$status->value = self::AS_PARSE_ERROR;
1687
			return $status;
1688
		}
1689
1690
		# Check image redirect
1691
		if ( $this->mTitle->getNamespace() == NS_FILE &&
1692
			$textbox_content->isRedirect() &&
1693
			!$wgUser->isAllowed( 'upload' )
1694
		) {
1695
				$code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1696
				$status->setResult( false, $code );
1697
1698
				return $status;
1699
		}
1700
1701
		# Check for spam
1702
		$match = self::matchSummarySpamRegex( $this->summary );
1703
		if ( $match === false && $this->section == 'new' ) {
1704
			# $wgSpamRegex is enforced on this new heading/summary because, unlike
1705
			# regular summaries, it is added to the actual wikitext.
1706
			if ( $this->sectiontitle !== '' ) {
1707
				# This branch is taken when the API is used with the 'sectiontitle' parameter.
1708
				$match = self::matchSpamRegex( $this->sectiontitle );
1709
			} else {
1710
				# This branch is taken when the "Add Topic" user interface is used, or the API
1711
				# is used with the 'summary' parameter.
1712
				$match = self::matchSpamRegex( $this->summary );
1713
			}
1714
		}
1715
		if ( $match === false ) {
1716
			$match = self::matchSpamRegex( $this->textbox1 );
1717
		}
1718
		if ( $match !== false ) {
1719
			$result['spam'] = $match;
1720
			$ip = $wgRequest->getIP();
1721
			$pdbk = $this->mTitle->getPrefixedDBkey();
1722
			$match = str_replace( "\n", '', $match );
1723
			wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1724
			$status->fatal( 'spamprotectionmatch', $match );
1725
			$status->value = self::AS_SPAM_ERROR;
1726
			return $status;
1727
		}
1728
		if ( !Hooks::run(
1729
			'EditFilter',
1730
			[ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1731
		) {
1732
			# Error messages etc. could be handled within the hook...
1733
			$status->fatal( 'hookaborted' );
1734
			$status->value = self::AS_HOOK_ERROR;
1735
			return $status;
1736
		} elseif ( $this->hookError != '' ) {
1737
			# ...or the hook could be expecting us to produce an error
1738
			$status->fatal( 'hookaborted' );
1739
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1740
			return $status;
1741
		}
1742
1743
		if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1744
			// Auto-block user's IP if the account was "hard" blocked
1745
			if ( !wfReadOnly() ) {
1746
				$wgUser->spreadAnyEditBlock();
1747
			}
1748
			# Check block state against master, thus 'false'.
1749
			$status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1750
			return $status;
1751
		}
1752
1753
		$this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->textbox1) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1754 View Code Duplication
		if ( $this->kblength > $wgMaxArticleSize ) {
1755
			// Error will be displayed by showEditForm()
1756
			$this->tooBig = true;
1757
			$status->setResult( false, self::AS_CONTENT_TOO_BIG );
1758
			return $status;
1759
		}
1760
1761 View Code Duplication
		if ( !$wgUser->isAllowed( 'edit' ) ) {
1762
			if ( $wgUser->isAnon() ) {
1763
				$status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1764
				return $status;
1765
			} else {
1766
				$status->fatal( 'readonlytext' );
1767
				$status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1768
				return $status;
1769
			}
1770
		}
1771
1772
		$changingContentModel = false;
1773
		if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1774 View Code Duplication
			if ( !$wgContentHandlerUseDB ) {
1775
				$status->fatal( 'editpage-cannot-use-custom-model' );
1776
				$status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1777
				return $status;
1778
			} elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1779
				$status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1780
				return $status;
1781
1782
			}
1783
			$changingContentModel = true;
1784
			$oldContentModel = $this->mTitle->getContentModel();
1785
		}
1786
1787
		if ( $this->changeTags ) {
1788
			$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1789
				$this->changeTags, $wgUser );
1790
			if ( !$changeTagsStatus->isOK() ) {
1791
				$changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1792
				return $changeTagsStatus;
1793
			}
1794
		}
1795
1796
		if ( wfReadOnly() ) {
1797
			$status->fatal( 'readonlytext' );
1798
			$status->value = self::AS_READ_ONLY_PAGE;
1799
			return $status;
1800
		}
1801
		if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1802
			$status->fatal( 'actionthrottledtext' );
1803
			$status->value = self::AS_RATE_LIMITED;
1804
			return $status;
1805
		}
1806
1807
		# If the article has been deleted while editing, don't save it without
1808
		# confirmation
1809
		if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1810
			$status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1811
			return $status;
1812
		}
1813
1814
		# Load the page data from the master. If anything changes in the meantime,
1815
		# we detect it by using page_latest like a token in a 1 try compare-and-swap.
1816
		$this->page->loadPageData( 'fromdbmaster' );
1817
		$new = !$this->page->exists();
1818
1819
		if ( $new ) {
1820
			// Late check for create permission, just in case *PARANOIA*
1821
			if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1822
				$status->fatal( 'nocreatetext' );
1823
				$status->value = self::AS_NO_CREATE_PERMISSION;
1824
				wfDebug( __METHOD__ . ": no create permission\n" );
1825
				return $status;
1826
			}
1827
1828
			// Don't save a new page if it's blank or if it's a MediaWiki:
1829
			// message with content equivalent to default (allow empty pages
1830
			// in this case to disable messages, see bug 50124)
1831
			$defaultMessageText = $this->mTitle->getDefaultMessageText();
1832
			if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1833
				$defaultText = $defaultMessageText;
1834
			} else {
1835
				$defaultText = '';
1836
			}
1837
1838
			if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1839
				$this->blankArticle = true;
1840
				$status->fatal( 'blankarticle' );
1841
				$status->setResult( false, self::AS_BLANK_ARTICLE );
1842
				return $status;
1843
			}
1844
1845
			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 1678 can be null; however, EditPage::runPostMergeFilters() 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...
1846
				return $status;
1847
			}
1848
1849
			$content = $textbox_content;
1850
1851
			$result['sectionanchor'] = '';
1852
			if ( $this->section == 'new' ) {
1853
				if ( $this->sectiontitle !== '' ) {
1854
					// Insert the section title above the content.
1855
					$content = $content->addSectionHeader( $this->sectiontitle );
1856
				} elseif ( $this->summary !== '' ) {
1857
					// Insert the section title above the content.
1858
					$content = $content->addSectionHeader( $this->summary );
1859
				}
1860
				$this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1861
			}
1862
1863
			$status->value = self::AS_SUCCESS_NEW_ARTICLE;
1864
1865
		} else { # not $new
1866
1867
			# Article exists. Check for edit conflict.
1868
1869
			$this->page->clear(); # Force reload of dates, etc.
1870
			$timestamp = $this->page->getTimestamp();
1871
			$latest = $this->page->getLatest();
1872
1873
			wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1874
1875
			// Check editRevId if set, which handles same-second timestamp collisions
1876
			if ( $timestamp != $this->edittime
1877
				|| ( $this->editRevId !== null && $this->editRevId != $latest )
1878
			) {
1879
				$this->isConflict = true;
1880
				if ( $this->section == 'new' ) {
1881
					if ( $this->page->getUserText() == $wgUser->getName() &&
1882
						$this->page->getComment() == $this->newSectionSummary()
1883
					) {
1884
						// Probably a duplicate submission of a new comment.
1885
						// This can happen when CDN resends a request after
1886
						// a timeout but the first one actually went through.
1887
						wfDebug( __METHOD__
1888
							. ": duplicate new section submission; trigger edit conflict!\n" );
1889
					} else {
1890
						// New comment; suppress conflict.
1891
						$this->isConflict = false;
1892
						wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1893
					}
1894
				} elseif ( $this->section == ''
1895
					&& Revision::userWasLastToEdit(
0 ignored issues
show
Deprecated Code introduced by
The method Revision::userWasLastToEdit() has been deprecated with message: since 1.24

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1896
						DB_MASTER, $this->mTitle->getArticleID(),
1897
						$wgUser->getId(), $this->edittime
1898
					)
1899
				) {
1900
					# Suppress edit conflict with self, except for section edits where merging is required.
1901
					wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1902
					$this->isConflict = false;
1903
				}
1904
			}
1905
1906
			// If sectiontitle is set, use it, otherwise use the summary as the section title.
1907
			if ( $this->sectiontitle !== '' ) {
1908
				$sectionTitle = $this->sectiontitle;
1909
			} else {
1910
				$sectionTitle = $this->summary;
1911
			}
1912
1913
			$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...
1914
1915
			if ( $this->isConflict ) {
1916
				wfDebug( __METHOD__
1917
					. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1918
					. " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
1919
				// @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
1920
				// ...or disable section editing for non-current revisions (not exposed anyway).
1921
				if ( $this->editRevId !== null ) {
1922
					$content = $this->page->replaceSectionAtRev(
1923
						$this->section,
1924
						$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1678 can be null; however, WikiPage::replaceSectionAtRev() 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...
1925
						$sectionTitle,
1926
						$this->editRevId
1927
					);
1928
				} else {
1929
					$content = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1930
						$this->section,
1931
						$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1678 can be null; however, WikiPage::replaceSectionContent() 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...
1932
						$sectionTitle,
1933
						$this->edittime
1934
					);
1935
				}
1936
			} else {
1937
				wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1938
				$content = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1939
					$this->section,
1940
					$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1678 can be null; however, WikiPage::replaceSectionContent() 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...
1941
					$sectionTitle
1942
				);
1943
			}
1944
1945
			if ( is_null( $content ) ) {
1946
				wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1947
				$this->isConflict = true;
1948
				$content = $textbox_content; // do not try to merge here!
1949
			} elseif ( $this->isConflict ) {
1950
				# Attempt merge
1951
				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...
1952
					// Successful merge! Maybe we should tell the user the good news?
1953
					$this->isConflict = false;
1954
					wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1955
				} else {
1956
					$this->section = '';
1957
					$this->textbox1 = ContentHandler::getContentText( $content );
1958
					wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1959
				}
1960
			}
1961
1962
			if ( $this->isConflict ) {
1963
				$status->setResult( false, self::AS_CONFLICT_DETECTED );
1964
				return $status;
1965
			}
1966
1967
			if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type null or string; 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...
1968
				return $status;
1969
			}
1970
1971
			if ( $this->section == 'new' ) {
1972
				// Handle the user preference to force summaries here
1973
				if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1974
					$this->missingSummary = true;
1975
					$status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
1976
					$status->value = self::AS_SUMMARY_NEEDED;
1977
					return $status;
1978
				}
1979
1980
				// Do not allow the user to post an empty comment
1981
				if ( $this->textbox1 == '' ) {
1982
					$this->missingComment = true;
1983
					$status->fatal( 'missingcommenttext' );
1984
					$status->value = self::AS_TEXTBOX_EMPTY;
1985
					return $status;
1986
				}
1987
			} elseif ( !$this->allowBlankSummary
1988
				&& !$content->equals( $this->getOriginalContent( $wgUser ) )
1989
				&& !$content->isRedirect()
1990
				&& md5( $this->summary ) == $this->autoSumm
1991
			) {
1992
				$this->missingSummary = true;
1993
				$status->fatal( 'missingsummary' );
1994
				$status->value = self::AS_SUMMARY_NEEDED;
1995
				return $status;
1996
			}
1997
1998
			# All's well
1999
			$sectionanchor = '';
2000
			if ( $this->section == 'new' ) {
2001
				$this->summary = $this->newSectionSummary( $sectionanchor );
2002
			} elseif ( $this->section != '' ) {
2003
				# Try to get a section anchor from the section source, redirect
2004
				# to edited section if header found.
2005
				# XXX: Might be better to integrate this into Article::replaceSectionAtRev
2006
				# for duplicate heading checking and maybe parsing.
2007
				$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2008
				# We can't deal with anchors, includes, html etc in the header for now,
2009
				# headline would need to be parsed to improve this.
2010
				if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2011
					$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
2012
				}
2013
			}
2014
			$result['sectionanchor'] = $sectionanchor;
2015
2016
			// Save errors may fall down to the edit form, but we've now
2017
			// merged the section into full text. Clear the section field
2018
			// so that later submission of conflict forms won't try to
2019
			// replace that into a duplicated mess.
2020
			$this->textbox1 = $this->toEditText( $content );
2021
			$this->section = '';
2022
2023
			$status->value = self::AS_SUCCESS_UPDATE;
2024
		}
2025
2026
		if ( !$this->allowSelfRedirect
2027
			&& $content->isRedirect()
2028
			&& $content->getRedirectTarget()->equals( $this->getTitle() )
2029
		) {
2030
			// If the page already redirects to itself, don't warn.
2031
			$currentTarget = $this->getCurrentContent()->getRedirectTarget();
2032
			if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2033
				$this->selfRedirect = true;
2034
				$status->fatal( 'selfredirect' );
2035
				$status->value = self::AS_SELF_REDIRECT;
2036
				return $status;
2037
			}
2038
		}
2039
2040
		// Check for length errors again now that the section is merged in
2041
		$this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->toE...Text($content)) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2042 View Code Duplication
		if ( $this->kblength > $wgMaxArticleSize ) {
2043
			$this->tooBig = true;
2044
			$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2045
			return $status;
2046
		}
2047
2048
		$flags = EDIT_AUTOSUMMARY |
2049
			( $new ? EDIT_NEW : EDIT_UPDATE ) |
2050
			( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2051
			( $bot ? EDIT_FORCE_BOT : 0 );
2052
2053
		$doEditStatus = $this->page->doEditContent(
2054
			$content,
2055
			$this->summary,
2056
			$flags,
2057
			false,
2058
			$wgUser,
2059
			$content->getDefaultFormat(),
2060
			$this->changeTags
2061
		);
2062
2063
		if ( !$doEditStatus->isOK() ) {
2064
			// Failure from doEdit()
2065
			// Show the edit conflict page for certain recognized errors from doEdit(),
2066
			// but don't show it for errors from extension hooks
2067
			$errors = $doEditStatus->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2068
			if ( in_array( $errors[0][0],
2069
					[ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2070
			) {
2071
				$this->isConflict = true;
2072
				// Destroys data doEdit() put in $status->value but who cares
2073
				$doEditStatus->value = self::AS_END;
2074
			}
2075
			return $doEditStatus;
2076
		}
2077
2078
		$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2079
		if ( $result['nullEdit'] ) {
2080
			// We don't know if it was a null edit until now, so increment here
2081
			$wgUser->pingLimiter( 'linkpurge' );
2082
		}
2083
		$result['redirect'] = $content->isRedirect();
2084
2085
		$this->updateWatchlist();
2086
2087
		// If the content model changed, add a log entry
2088
		if ( $changingContentModel ) {
2089
			$this->addContentModelChangeLogEntry(
2090
				$wgUser,
2091
				$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...
2092
				$this->contentModel,
2093
				$this->summary
2094
			);
2095
		}
2096
2097
		return $status;
2098
	}
2099
2100
	/**
2101
	 * @param User $user
2102
	 * @param string|false $oldModel false if the page is being newly created
2103
	 * @param string $newModel
2104
	 * @param string $reason
2105
	 */
2106
	protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2107
		$new = $oldModel === false;
2108
		$log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2109
		$log->setPerformer( $user );
2110
		$log->setTarget( $this->mTitle );
2111
		$log->setComment( $reason );
2112
		$log->setParameters( [
2113
			'4::oldmodel' => $oldModel,
2114
			'5::newmodel' => $newModel
2115
		] );
2116
		$logid = $log->insert();
2117
		$log->publish( $logid );
2118
	}
2119
2120
	/**
2121
	 * Register the change of watch status
2122
	 */
2123
	protected function updateWatchlist() {
2124
		global $wgUser;
2125
2126
		if ( !$wgUser->isLoggedIn() ) {
2127
			return;
2128
		}
2129
2130
		$user = $wgUser;
2131
		$title = $this->mTitle;
2132
		$watch = $this->watchthis;
2133
		// Do this in its own transaction to reduce contention...
2134
		DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2135
			if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2136
				return; // nothing to change
2137
			}
2138
			WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2139
		} );
2140
	}
2141
2142
	/**
2143
	 * Attempts to do 3-way merge of edit content with a base revision
2144
	 * and current content, in case of edit conflict, in whichever way appropriate
2145
	 * for the content type.
2146
	 *
2147
	 * @since 1.21
2148
	 *
2149
	 * @param Content $editContent
2150
	 *
2151
	 * @return bool
2152
	 */
2153
	private function mergeChangesIntoContent( &$editContent ) {
2154
2155
		$db = wfGetDB( DB_MASTER );
2156
2157
		// This is the revision the editor started from
2158
		$baseRevision = $this->getBaseRevision();
2159
		$baseContent = $baseRevision ? $baseRevision->getContent() : null;
2160
2161
		if ( is_null( $baseContent ) ) {
2162
			return false;
2163
		}
2164
2165
		// The current state, we want to merge updates into it
2166
		$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2167
		$currentContent = $currentRevision ? $currentRevision->getContent() : null;
2168
2169
		if ( is_null( $currentContent ) ) {
2170
			return false;
2171
		}
2172
2173
		$handler = ContentHandler::getForModelID( $baseContent->getModel() );
2174
2175
		$result = $handler->merge3( $baseContent, $editContent, $currentContent );
2176
2177
		if ( $result ) {
2178
			$editContent = $result;
2179
			// Update parentRevId to what we just merged.
2180
			$this->parentRevId = $currentRevision->getId();
2181
			return true;
2182
		}
2183
2184
		return false;
2185
	}
2186
2187
	/**
2188
	 * @note: this method is very poorly named. If the user opened the form with ?oldid=X,
2189
	 *        one might think of X as the "base revision", which is NOT what this returns.
2190
	 * @return Revision Current version when the edit was started
2191
	 */
2192
	function getBaseRevision() {
2193
		if ( !$this->mBaseRevision ) {
2194
			$db = wfGetDB( DB_MASTER );
2195
			$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...
2196
				? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2197
				: Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2198
		}
2199
		return $this->mBaseRevision;
2200
	}
2201
2202
	/**
2203
	 * Check given input text against $wgSpamRegex, and return the text of the first match.
2204
	 *
2205
	 * @param string $text
2206
	 *
2207
	 * @return string|bool Matching string or false
2208
	 */
2209
	public static function matchSpamRegex( $text ) {
2210
		global $wgSpamRegex;
2211
		// For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2212
		$regexes = (array)$wgSpamRegex;
2213
		return self::matchSpamRegexInternal( $text, $regexes );
2214
	}
2215
2216
	/**
2217
	 * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
2218
	 *
2219
	 * @param string $text
2220
	 *
2221
	 * @return string|bool Matching string or false
2222
	 */
2223
	public static function matchSummarySpamRegex( $text ) {
2224
		global $wgSummarySpamRegex;
2225
		$regexes = (array)$wgSummarySpamRegex;
2226
		return self::matchSpamRegexInternal( $text, $regexes );
2227
	}
2228
2229
	/**
2230
	 * @param string $text
2231
	 * @param array $regexes
2232
	 * @return bool|string
2233
	 */
2234
	protected static function matchSpamRegexInternal( $text, $regexes ) {
2235
		foreach ( $regexes as $regex ) {
2236
			$matches = [];
2237
			if ( preg_match( $regex, $text, $matches ) ) {
2238
				return $matches[0];
2239
			}
2240
		}
2241
		return false;
2242
	}
2243
2244
	function setHeaders() {
2245
		global $wgOut, $wgUser, $wgAjaxEditStash;
2246
2247
		$wgOut->addModules( 'mediawiki.action.edit' );
2248
		$wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2249
2250
		if ( $wgUser->getOption( 'showtoolbar' ) ) {
2251
			// The addition of default buttons is handled by getEditToolbar() which
2252
			// has its own dependency on this module. The call here ensures the module
2253
			// is loaded in time (it has position "top") for other modules to register
2254
			// buttons (e.g. extensions, gadgets, user scripts).
2255
			$wgOut->addModules( 'mediawiki.toolbar' );
2256
		}
2257
2258
		if ( $wgUser->getOption( 'uselivepreview' ) ) {
2259
			$wgOut->addModules( 'mediawiki.action.edit.preview' );
2260
		}
2261
2262
		if ( $wgUser->getOption( 'useeditwarning' ) ) {
2263
			$wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2264
		}
2265
2266
		# Enabled article-related sidebar, toplinks, etc.
2267
		$wgOut->setArticleRelated( true );
2268
2269
		$contextTitle = $this->getContextTitle();
2270
		if ( $this->isConflict ) {
2271
			$msg = 'editconflict';
2272
		} elseif ( $contextTitle->exists() && $this->section != '' ) {
2273
			$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2274
		} else {
2275
			$msg = $contextTitle->exists()
2276
				|| ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2277
					&& $contextTitle->getDefaultMessageText() !== false
2278
				)
2279
				? 'editing'
2280
				: 'creating';
2281
		}
2282
2283
		# Use the title defined by DISPLAYTITLE magic word when present
2284
		# NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2285
		#       setPageTitle() treats the input as wikitext, which should be safe in either case.
2286
		$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2287
		if ( $displayTitle === false ) {
2288
			$displayTitle = $contextTitle->getPrefixedText();
2289
		}
2290
		$wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2291
		# Transmit the name of the message to JavaScript for live preview
2292
		# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2293
		$wgOut->addJsConfigVars( [
2294
			'wgEditMessage' => $msg,
2295
			'wgAjaxEditStash' => $wgAjaxEditStash,
2296
		] );
2297
	}
2298
2299
	/**
2300
	 * Show all applicable editing introductions
2301
	 */
2302
	protected function showIntro() {
2303
		global $wgOut, $wgUser;
2304
		if ( $this->suppressIntro ) {
2305
			return;
2306
		}
2307
2308
		$namespace = $this->mTitle->getNamespace();
2309
2310
		if ( $namespace == NS_MEDIAWIKI ) {
2311
			# Show a warning if editing an interface message
2312
			$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2313
			# If this is a default message (but not css or js),
2314
			# show a hint that it is translatable on translatewiki.net
2315
			if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2316
				&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2317
			) {
2318
				$defaultMessageText = $this->mTitle->getDefaultMessageText();
2319
				if ( $defaultMessageText !== false ) {
2320
					$wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2321
						'translateinterface' );
2322
				}
2323
			}
2324
		} elseif ( $namespace == NS_FILE ) {
2325
			# Show a hint to shared repo
2326
			$file = wfFindFile( $this->mTitle );
2327
			if ( $file && !$file->isLocal() ) {
2328
				$descUrl = $file->getDescriptionUrl();
2329
				# there must be a description url to show a hint to shared repo
2330
				if ( $descUrl ) {
2331
					if ( !$this->mTitle->exists() ) {
2332
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2333
									'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2334
						] );
2335
					} else {
2336
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2337
									'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2338
						] );
2339
					}
2340
				}
2341
			}
2342
		}
2343
2344
		# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2345
		# Show log extract when the user is currently blocked
2346
		if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2347
			$username = explode( '/', $this->mTitle->getText(), 2 )[0];
2348
			$user = User::newFromName( $username, false /* allow IP users*/ );
2349
			$ip = User::isIP( $username );
2350
			$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 2348 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 2348 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...
2351
			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2352
				$wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2353
					[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2354 View Code Duplication
			} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2355
				# Show log extract if the user is currently blocked
2356
				LogEventsList::showLogExtract(
2357
					$wgOut,
2358
					'block',
2359
					MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2360
					'',
2361
					[
2362
						'lim' => 1,
2363
						'showIfEmpty' => false,
2364
						'msgKey' => [
2365
							'blocked-notice-logextract',
2366
							$user->getName() # Support GENDER in notice
2367
						]
2368
					]
2369
				);
2370
			}
2371
		}
2372
		# Try to add a custom edit intro, or use the standard one if this is not possible.
2373
		if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2374
			$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2375
				wfMessage( 'helppage' )->inContentLanguage()->text()
2376
			) );
2377
			if ( $wgUser->isLoggedIn() ) {
2378
				$wgOut->wrapWikiMsg(
2379
					// Suppress the external link icon, consider the help url an internal one
2380
					"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2381
					[
2382
						'newarticletext',
2383
						$helpLink
2384
					]
2385
				);
2386
			} else {
2387
				$wgOut->wrapWikiMsg(
2388
					// Suppress the external link icon, consider the help url an internal one
2389
					"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2390
					[
2391
						'newarticletextanon',
2392
						$helpLink
2393
					]
2394
				);
2395
			}
2396
		}
2397
		# Give a notice if the user is editing a deleted/moved page...
2398 View Code Duplication
		if ( !$this->mTitle->exists() ) {
2399
			LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2400
				'',
2401
				[
2402
					'lim' => 10,
2403
					'conds' => [ "log_action != 'revision'" ],
2404
					'showIfEmpty' => false,
2405
					'msgKey' => [ 'recreate-moveddeleted-warn' ]
2406
				]
2407
			);
2408
		}
2409
	}
2410
2411
	/**
2412
	 * Attempt to show a custom editing introduction, if supplied
2413
	 *
2414
	 * @return bool
2415
	 */
2416
	protected function showCustomIntro() {
2417
		if ( $this->editintro ) {
2418
			$title = Title::newFromText( $this->editintro );
2419
			if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2420
				global $wgOut;
2421
				// Added using template syntax, to take <noinclude>'s into account.
2422
				$wgOut->addWikiTextTitleTidy(
2423
					'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2424
					$this->mTitle
2425
				);
2426
				return true;
2427
			}
2428
		}
2429
		return false;
2430
	}
2431
2432
	/**
2433
	 * Gets an editable textual representation of $content.
2434
	 * The textual representation can be turned by into a Content object by the
2435
	 * toEditContent() method.
2436
	 *
2437
	 * If $content is null or false or a string, $content is returned unchanged.
2438
	 *
2439
	 * If the given Content object is not of a type that can be edited using
2440
	 * the text base EditPage, an exception will be raised. Set
2441
	 * $this->allowNonTextContent to true to allow editing of non-textual
2442
	 * content.
2443
	 *
2444
	 * @param Content|null|bool|string $content
2445
	 * @return string The editable text form of the content.
2446
	 *
2447
	 * @throws MWException If $content is not an instance of TextContent and
2448
	 *   $this->allowNonTextContent is not true.
2449
	 */
2450
	protected function toEditText( $content ) {
2451
		if ( $content === null || $content === false || is_string( $content ) ) {
2452
			return $content;
2453
		}
2454
2455
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2456
			throw new MWException( 'This content model is not supported: '
2457
				. ContentHandler::getLocalizedName( $content->getModel() ) );
0 ignored issues
show
Bug introduced by
It seems like $content is not always an object, but can also be of type boolean. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
2458
		}
2459
2460
		return $content->serialize( $this->contentFormat );
2461
	}
2462
2463
	/**
2464
	 * Turns the given text into a Content object by unserializing it.
2465
	 *
2466
	 * If the resulting Content object is not of a type that can be edited using
2467
	 * the text base EditPage, an exception will be raised. Set
2468
	 * $this->allowNonTextContent to true to allow editing of non-textual
2469
	 * content.
2470
	 *
2471
	 * @param string|null|bool $text Text to unserialize
2472
	 * @return Content The content object created from $text. If $text was false
2473
	 *   or null, false resp. null will be  returned instead.
2474
	 *
2475
	 * @throws MWException If unserializing the text results in a Content
2476
	 *   object that is not an instance of TextContent and
2477
	 *   $this->allowNonTextContent is not true.
2478
	 */
2479
	protected function toEditContent( $text ) {
2480
		if ( $text === false || $text === null ) {
2481
			return $text;
2482
		}
2483
2484
		$content = ContentHandler::makeContent( $text, $this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $text defined by parameter $text on line 2479 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...
2485
			$this->contentModel, $this->contentFormat );
2486
2487
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2488
			throw new MWException( 'This content model is not supported: '
2489
				. ContentHandler::getLocalizedName( $content->getModel() ) );
2490
		}
2491
2492
		return $content;
2493
	}
2494
2495
	/**
2496
	 * Send the edit form and related headers to $wgOut
2497
	 * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2498
	 *     during form output near the top, for captchas and the like.
2499
	 *
2500
	 * The $formCallback parameter is deprecated since MediaWiki 1.25. Please
2501
	 * use the EditPage::showEditForm:fields hook instead.
2502
	 */
2503
	function showEditForm( $formCallback = null ) {
2504
		global $wgOut, $wgUser;
2505
2506
		# need to parse the preview early so that we know which templates are used,
2507
		# otherwise users with "show preview after edit box" will get a blank list
2508
		# we parse this near the beginning so that setHeaders can do the title
2509
		# setting work instead of leaving it in getPreviewText
2510
		$previewOutput = '';
2511
		if ( $this->formtype == 'preview' ) {
2512
			$previewOutput = $this->getPreviewText();
2513
		}
2514
2515
		Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2516
2517
		$this->setHeaders();
2518
2519
		if ( $this->showHeader() === false ) {
2520
			return;
2521
		}
2522
2523
		$wgOut->addHTML( $this->editFormPageTop );
2524
2525
		if ( $wgUser->getOption( 'previewontop' ) ) {
2526
			$this->displayPreviewArea( $previewOutput, true );
2527
		}
2528
2529
		$wgOut->addHTML( $this->editFormTextTop );
2530
2531
		$showToolbar = true;
2532
		if ( $this->wasDeletedSinceLastEdit() ) {
2533
			if ( $this->formtype == 'save' ) {
2534
				// Hide the toolbar and edit area, user can click preview to get it back
2535
				// Add an confirmation checkbox and explanation.
2536
				$showToolbar = false;
2537
			} else {
2538
				$wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2539
					'deletedwhileediting' );
2540
			}
2541
		}
2542
2543
		// @todo add EditForm plugin interface and use it here!
2544
		//       search for textarea1 and textares2, and allow EditForm to override all uses.
2545
		$wgOut->addHTML( Html::openElement(
2546
			'form',
2547
			[
2548
				'id' => self::EDITFORM_ID,
2549
				'name' => self::EDITFORM_ID,
2550
				'method' => 'post',
2551
				'action' => $this->getActionURL( $this->getContextTitle() ),
2552
				'enctype' => 'multipart/form-data'
2553
			]
2554
		) );
2555
2556
		if ( is_callable( $formCallback ) ) {
2557
			wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2558
			call_user_func_array( $formCallback, [ &$wgOut ] );
2559
		}
2560
2561
		// Add an empty field to trip up spambots
2562
		$wgOut->addHTML(
2563
			Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2564
			. Html::rawElement(
2565
				'label',
2566
				[ 'for' => 'wpAntispam' ],
2567
				wfMessage( 'simpleantispam-label' )->parse()
2568
			)
2569
			. Xml::element(
2570
				'input',
2571
				[
2572
					'type' => 'text',
2573
					'name' => 'wpAntispam',
2574
					'id' => 'wpAntispam',
2575
					'value' => ''
2576
				]
2577
			)
2578
			. Xml::closeElement( 'div' )
2579
		);
2580
2581
		Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2582
2583
		// Put these up at the top to ensure they aren't lost on early form submission
2584
		$this->showFormBeforeText();
2585
2586
		if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2587
			$username = $this->lastDelete->user_name;
2588
			$comment = $this->lastDelete->log_comment;
2589
2590
			// It is better to not parse the comment at all than to have templates expanded in the middle
2591
			// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2592
			$key = $comment === ''
2593
				? 'confirmrecreate-noreason'
2594
				: 'confirmrecreate';
2595
			$wgOut->addHTML(
2596
				'<div class="mw-confirm-recreate">' .
2597
					wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2598
				Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2599
					[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2600
				) .
2601
				'</div>'
2602
			);
2603
		}
2604
2605
		# When the summary is hidden, also hide them on preview/show changes
2606
		if ( $this->nosummary ) {
2607
			$wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2608
		}
2609
2610
		# If a blank edit summary was previously provided, and the appropriate
2611
		# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2612
		# user being bounced back more than once in the event that a summary
2613
		# is not required.
2614
		# ####
2615
		# For a bit more sophisticated detection of blank summaries, hash the
2616
		# automatic one and pass that in the hidden field wpAutoSummary.
2617
		if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2618
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2619
		}
2620
2621
		if ( $this->undidRev ) {
2622
			$wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2623
		}
2624
2625
		if ( $this->selfRedirect ) {
2626
			$wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2627
		}
2628
2629
		if ( $this->hasPresetSummary ) {
2630
			// If a summary has been preset using &summary= we don't want to prompt for
2631
			// a different summary. Only prompt for a summary if the summary is blanked.
2632
			// (Bug 17416)
2633
			$this->autoSumm = md5( '' );
2634
		}
2635
2636
		$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2637
		$wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2638
2639
		$wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2640
		$wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2641
2642
		$wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2643
		$wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2644
2645 View Code Duplication
		if ( $this->section == 'new' ) {
2646
			$this->showSummaryInput( true, $this->summary );
2647
			$wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2648
		}
2649
2650
		$wgOut->addHTML( $this->editFormTextBeforeContent );
2651
2652
		if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2653
			$wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2654
		}
2655
2656
		if ( $this->blankArticle ) {
2657
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2658
		}
2659
2660
		if ( $this->isConflict ) {
2661
			// In an edit conflict bypass the overridable content form method
2662
			// and fallback to the raw wpTextbox1 since editconflicts can't be
2663
			// resolved between page source edits and custom ui edits using the
2664
			// custom edit ui.
2665
			$this->textbox2 = $this->textbox1;
2666
2667
			$content = $this->getCurrentContent();
2668
			$this->textbox1 = $this->toEditText( $content );
2669
2670
			$this->showTextbox1();
2671
		} else {
2672
			$this->showContentForm();
2673
		}
2674
2675
		$wgOut->addHTML( $this->editFormTextAfterContent );
2676
2677
		$this->showStandardInputs();
2678
2679
		$this->showFormAfterText();
2680
2681
		$this->showTosSummary();
2682
2683
		$this->showEditTools();
2684
2685
		$wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2686
2687
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2688
			Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2689
2690
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2691
			Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2692
2693
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2694
			self::getPreviewLimitReport( $this->mParserOutput ) ) );
2695
2696
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2697
2698 View Code Duplication
		if ( $this->isConflict ) {
2699
			try {
2700
				$this->showConflict();
2701
			} catch ( MWContentSerializationException $ex ) {
2702
				// this can't really happen, but be nice if it does.
2703
				$msg = wfMessage(
2704
					'content-failed-to-parse',
2705
					$this->contentModel,
2706
					$this->contentFormat,
2707
					$ex->getMessage()
2708
				);
2709
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2710
			}
2711
		}
2712
2713
		// Set a hidden field so JS knows what edit form mode we are in
2714
		if ( $this->isConflict ) {
2715
			$mode = 'conflict';
2716
		} elseif ( $this->preview ) {
2717
			$mode = 'preview';
2718
		} elseif ( $this->diff ) {
2719
			$mode = 'diff';
2720
		} else {
2721
			$mode = 'text';
2722
		}
2723
		$wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2724
2725
		// Marker for detecting truncated form data.  This must be the last
2726
		// parameter sent in order to be of use, so do not move me.
2727
		$wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2728
		$wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2729
2730
		if ( !$wgUser->getOption( 'previewontop' ) ) {
2731
			$this->displayPreviewArea( $previewOutput, false );
2732
		}
2733
2734
	}
2735
2736
	/**
2737
	 * Extract the section title from current section text, if any.
2738
	 *
2739
	 * @param string $text
2740
	 * @return string|bool String or false
2741
	 */
2742
	public static function extractSectionTitle( $text ) {
2743
		preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2744
		if ( !empty( $matches[2] ) ) {
2745
			global $wgParser;
2746
			return $wgParser->stripSectionName( trim( $matches[2] ) );
2747
		} else {
2748
			return false;
2749
		}
2750
	}
2751
2752
	/**
2753
	 * @return bool
2754
	 */
2755
	protected function showHeader() {
2756
		global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
2757
		global $wgAllowUserCss, $wgAllowUserJs;
2758
2759
		if ( $this->mTitle->isTalkPage() ) {
2760
			$wgOut->addWikiMsg( 'talkpagetext' );
2761
		}
2762
2763
		// Add edit notices
2764
		$editNotices = $this->mTitle->getEditNotices( $this->oldid );
2765
		if ( count( $editNotices ) ) {
2766
			$wgOut->addHTML( implode( "\n", $editNotices ) );
2767
		} else {
2768
			$msg = wfMessage( 'editnotice-notext' );
2769
			if ( !$msg->isDisabled() ) {
2770
				$wgOut->addHTML(
2771
					'<div class="mw-editnotice-notext">'
2772
					. $msg->parseAsBlock()
2773
					. '</div>'
2774
				);
2775
			}
2776
		}
2777
2778
		if ( $this->isConflict ) {
2779
			$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2780
			$this->editRevId = $this->page->getLatest();
2781
		} else {
2782
			if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2783
				// We use $this->section to much before this and getVal('wgSection') directly in other places
2784
				// at this point we can't reset $this->section to '' to fallback to non-section editing.
2785
				// Someone is welcome to try refactoring though
2786
				$wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2787
				return false;
2788
			}
2789
2790
			if ( $this->section != '' && $this->section != 'new' ) {
2791
				if ( !$this->summary && !$this->preview && !$this->diff ) {
2792
					$sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2793
					if ( $sectionTitle !== false ) {
2794
						$this->summary = "/* $sectionTitle */ ";
2795
					}
2796
				}
2797
			}
2798
2799
			if ( $this->missingComment ) {
2800
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2801
			}
2802
2803
			if ( $this->missingSummary && $this->section != 'new' ) {
2804
				$wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2805
			}
2806
2807
			if ( $this->missingSummary && $this->section == 'new' ) {
2808
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2809
			}
2810
2811
			if ( $this->blankArticle ) {
2812
				$wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2813
			}
2814
2815
			if ( $this->selfRedirect ) {
2816
				$wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2817
			}
2818
2819
			if ( $this->hookError !== '' ) {
2820
				$wgOut->addWikiText( $this->hookError );
2821
			}
2822
2823
			if ( !$this->checkUnicodeCompliantBrowser() ) {
2824
				$wgOut->addWikiMsg( 'nonunicodebrowser' );
2825
			}
2826
2827
			if ( $this->section != 'new' ) {
2828
				$revision = $this->mArticle->getRevisionFetched();
2829
				if ( $revision ) {
2830
					// Let sysop know that this will make private content public if saved
2831
2832 View Code Duplication
					if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2833
						$wgOut->wrapWikiMsg(
2834
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2835
							'rev-deleted-text-permission'
2836
						);
2837
					} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2838
						$wgOut->wrapWikiMsg(
2839
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2840
							'rev-deleted-text-view'
2841
						);
2842
					}
2843
2844
					if ( !$revision->isCurrent() ) {
2845
						$this->mArticle->setOldSubtitle( $revision->getId() );
2846
						$wgOut->addWikiMsg( 'editingold' );
2847
					}
2848
				} elseif ( $this->mTitle->exists() ) {
2849
					// Something went wrong
2850
2851
					$wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2852
						[ 'missing-revision', $this->oldid ] );
2853
				}
2854
			}
2855
		}
2856
2857
		if ( wfReadOnly() ) {
2858
			$wgOut->wrapWikiMsg(
2859
				"<div id=\"mw-read-only-warning\">\n$1\n</div>",
2860
				[ 'readonlywarning', wfReadOnlyReason() ]
2861
			);
2862
		} elseif ( $wgUser->isAnon() ) {
2863
			if ( $this->formtype != 'preview' ) {
2864
				$wgOut->wrapWikiMsg(
2865
					"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2866
					[ 'anoneditwarning',
2867
						// Log-in link
2868
						SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2869
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2870
						] ),
2871
						// Sign-up link
2872
						SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2873
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2874
						] )
2875
					]
2876
				);
2877
			} else {
2878
				$wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2879
					'anonpreviewwarning'
2880
				);
2881
			}
2882
		} else {
2883
			if ( $this->isCssJsSubpage ) {
2884
				# Check the skin exists
2885
				if ( $this->isWrongCaseCssJsPage ) {
2886
					$wgOut->wrapWikiMsg(
2887
						"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2888
						[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2889
					);
2890
				}
2891
				if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2892
					if ( $this->formtype !== 'preview' ) {
2893
						if ( $this->isCssSubpage && $wgAllowUserCss ) {
2894
							$wgOut->wrapWikiMsg(
2895
								"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2896
								[ 'usercssyoucanpreview' ]
2897
							);
2898
						}
2899
2900
						if ( $this->isJsSubpage && $wgAllowUserJs ) {
2901
							$wgOut->wrapWikiMsg(
2902
								"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2903
								[ 'userjsyoucanpreview' ]
2904
							);
2905
						}
2906
					}
2907
				}
2908
			}
2909
		}
2910
2911
		if ( $this->mTitle->isProtected( 'edit' ) &&
2912
			MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2913
		) {
2914
			# Is the title semi-protected?
2915
			if ( $this->mTitle->isSemiProtected() ) {
2916
				$noticeMsg = 'semiprotectedpagewarning';
2917
			} else {
2918
				# Then it must be protected based on static groups (regular)
2919
				$noticeMsg = 'protectedpagewarning';
2920
			}
2921
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2922
				[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2923
		}
2924
		if ( $this->mTitle->isCascadeProtected() ) {
2925
			# Is this page under cascading protection from some source pages?
2926
			/** @var Title[] $cascadeSources */
2927
			list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2928
			$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2929
			$cascadeSourcesCount = count( $cascadeSources );
2930
			if ( $cascadeSourcesCount > 0 ) {
2931
				# Explain, and list the titles responsible
2932
				foreach ( $cascadeSources as $page ) {
2933
					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2934
				}
2935
			}
2936
			$notice .= '</div>';
2937
			$wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2938
		}
2939
		if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2940
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2941
				[ 'lim' => 1,
2942
					'showIfEmpty' => false,
2943
					'msgKey' => [ 'titleprotectedwarning' ],
2944
					'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2945
		}
2946
2947
		if ( $this->kblength === false ) {
2948
			$this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->textbox1) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2949
		}
2950
2951
		if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2952
			$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2953
				[
2954
					'longpageerror',
2955
					$wgLang->formatNum( $this->kblength ),
2956
					$wgLang->formatNum( $wgMaxArticleSize )
2957
				]
2958
			);
2959
		} else {
2960
			if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2961
				$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2962
					[
2963
						'longpage-hint',
2964
						$wgLang->formatSize( strlen( $this->textbox1 ) ),
2965
						strlen( $this->textbox1 )
2966
					]
2967
				);
2968
			}
2969
		}
2970
		# Add header copyright warning
2971
		$this->showHeaderCopyrightWarning();
2972
2973
		return true;
2974
	}
2975
2976
	/**
2977
	 * Standard summary input and label (wgSummary), abstracted so EditPage
2978
	 * subclasses may reorganize the form.
2979
	 * Note that you do not need to worry about the label's for=, it will be
2980
	 * inferred by the id given to the input. You can remove them both by
2981
	 * passing array( 'id' => false ) to $userInputAttrs.
2982
	 *
2983
	 * @param string $summary The value of the summary input
2984
	 * @param string $labelText The html to place inside the label
2985
	 * @param array $inputAttrs Array of attrs to use on the input
2986
	 * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
2987
	 *
2988
	 * @return array An array in the format array( $label, $input )
2989
	 */
2990
	function getSummaryInput( $summary = "", $labelText = null,
2991
		$inputAttrs = null, $spanLabelAttrs = null
2992
	) {
2993
		// Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2994
		$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2995
			'id' => 'wpSummary',
2996
			'maxlength' => '200',
2997
			'tabindex' => '1',
2998
			'size' => 60,
2999
			'spellcheck' => 'true',
3000
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
3001
3002
		$spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3003
			'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3004
			'id' => "wpSummaryLabel"
3005
		];
3006
3007
		$label = null;
3008
		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...
3009
			$label = Xml::tags(
3010
				'label',
3011
				$inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3012
				$labelText
3013
			);
3014
			$label = Xml::tags( 'span', $spanLabelAttrs, $label );
3015
		}
3016
3017
		$input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3018
3019
		return [ $label, $input ];
3020
	}
3021
3022
	/**
3023
	 * @param bool $isSubjectPreview True if this is the section subject/title
3024
	 *   up top, or false if this is the comment summary
3025
	 *   down below the textarea
3026
	 * @param string $summary The text of the summary to display
3027
	 */
3028
	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3029
		global $wgOut, $wgContLang;
3030
		# Add a class if 'missingsummary' is triggered to allow styling of the summary line
3031
		$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3032
		if ( $isSubjectPreview ) {
3033
			if ( $this->nosummary ) {
3034
				return;
3035
			}
3036
		} else {
3037
			if ( !$this->mShowSummaryField ) {
3038
				return;
3039
			}
3040
		}
3041
		$summary = $wgContLang->recodeForEdit( $summary );
3042
		$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3043
		list( $label, $input ) = $this->getSummaryInput(
3044
			$summary,
3045
			$labelText,
3046
			[ 'class' => $summaryClass ],
3047
			[]
3048
		);
3049
		$wgOut->addHTML( "{$label} {$input}" );
3050
	}
3051
3052
	/**
3053
	 * @param bool $isSubjectPreview True if this is the section subject/title
3054
	 *   up top, or false if this is the comment summary
3055
	 *   down below the textarea
3056
	 * @param string $summary The text of the summary to display
3057
	 * @return string
3058
	 */
3059
	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3060
		// avoid spaces in preview, gets always trimmed on save
3061
		$summary = trim( $summary );
3062
		if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3063
			return "";
3064
		}
3065
3066
		global $wgParser;
3067
3068
		if ( $isSubjectPreview ) {
3069
			$summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3070
				->inContentLanguage()->text();
3071
		}
3072
3073
		$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3074
3075
		$summary = wfMessage( $message )->parse()
3076
			. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3077
		return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3078
	}
3079
3080
	protected function showFormBeforeText() {
3081
		global $wgOut;
3082
		$section = htmlspecialchars( $this->section );
3083
		$wgOut->addHTML( <<<HTML
3084
<input type='hidden' value="{$section}" name="wpSection"/>
3085
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3086
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3087
<input type='hidden' value="{$this->editRevId}" name="editRevId" />
3088
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3089
3090
HTML
3091
		);
3092
		if ( !$this->checkUnicodeCompliantBrowser() ) {
3093
			$wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3094
		}
3095
	}
3096
3097
	protected function showFormAfterText() {
3098
		global $wgOut, $wgUser;
3099
		/**
3100
		 * To make it harder for someone to slip a user a page
3101
		 * which submits an edit form to the wiki without their
3102
		 * knowledge, a random token is associated with the login
3103
		 * session. If it's not passed back with the submission,
3104
		 * we won't save the page, or render user JavaScript and
3105
		 * CSS previews.
3106
		 *
3107
		 * For anon editors, who may not have a session, we just
3108
		 * include the constant suffix to prevent editing from
3109
		 * broken text-mangling proxies.
3110
		 */
3111
		$wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3112
	}
3113
3114
	/**
3115
	 * Subpage overridable method for printing the form for page content editing
3116
	 * By default this simply outputs wpTextbox1
3117
	 * Subclasses can override this to provide a custom UI for editing;
3118
	 * be it a form, or simply wpTextbox1 with a modified content that will be
3119
	 * reverse modified when extracted from the post data.
3120
	 * Note that this is basically the inverse for importContentFormData
3121
	 */
3122
	protected function showContentForm() {
3123
		$this->showTextbox1();
3124
	}
3125
3126
	/**
3127
	 * Method to output wpTextbox1
3128
	 * The $textoverride method can be used by subclasses overriding showContentForm
3129
	 * to pass back to this method.
3130
	 *
3131
	 * @param array $customAttribs Array of html attributes to use in the textarea
3132
	 * @param string $textoverride Optional text to override $this->textarea1 with
3133
	 */
3134
	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3135
		if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3136
			$attribs = [ 'style' => 'display:none;' ];
3137
		} else {
3138
			$classes = []; // Textarea CSS
3139
			if ( $this->mTitle->isProtected( 'edit' ) &&
3140
				MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3141
			) {
3142
				# Is the title semi-protected?
3143
				if ( $this->mTitle->isSemiProtected() ) {
3144
					$classes[] = 'mw-textarea-sprotected';
3145
				} else {
3146
					# Then it must be protected based on static groups (regular)
3147
					$classes[] = 'mw-textarea-protected';
3148
				}
3149
				# Is the title cascade-protected?
3150
				if ( $this->mTitle->isCascadeProtected() ) {
3151
					$classes[] = 'mw-textarea-cprotected';
3152
				}
3153
			}
3154
3155
			$attribs = [ 'tabindex' => 1 ];
3156
3157
			if ( is_array( $customAttribs ) ) {
3158
				$attribs += $customAttribs;
3159
			}
3160
3161
			if ( count( $classes ) ) {
3162
				if ( isset( $attribs['class'] ) ) {
3163
					$classes[] = $attribs['class'];
3164
				}
3165
				$attribs['class'] = implode( ' ', $classes );
3166
			}
3167
		}
3168
3169
		$this->showTextbox(
3170
			$textoverride !== null ? $textoverride : $this->textbox1,
3171
			'wpTextbox1',
3172
			$attribs
3173
		);
3174
	}
3175
3176
	protected function showTextbox2() {
3177
		$this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3178
	}
3179
3180
	protected function showTextbox( $text, $name, $customAttribs = [] ) {
3181
		global $wgOut, $wgUser;
3182
3183
		$wikitext = $this->safeUnicodeOutput( $text );
3184
		if ( strval( $wikitext ) !== '' ) {
3185
			// Ensure there's a newline at the end, otherwise adding lines
3186
			// is awkward.
3187
			// But don't add a newline if the ext is empty, or Firefox in XHTML
3188
			// mode will show an extra newline. A bit annoying.
3189
			$wikitext .= "\n";
3190
		}
3191
3192
		$attribs = $customAttribs + [
3193
			'accesskey' => ',',
3194
			'id' => $name,
3195
			'cols' => $wgUser->getIntOption( 'cols' ),
3196
			'rows' => $wgUser->getIntOption( 'rows' ),
3197
			// Avoid PHP notices when appending preferences
3198
			// (appending allows customAttribs['style'] to still work).
3199
			'style' => ''
3200
		];
3201
3202
		$pageLang = $this->mTitle->getPageLanguage();
3203
		$attribs['lang'] = $pageLang->getHtmlCode();
3204
		$attribs['dir'] = $pageLang->getDir();
3205
3206
		$wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3207
	}
3208
3209
	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3210
		global $wgOut;
3211
		$classes = [];
3212
		if ( $isOnTop ) {
3213
			$classes[] = 'ontop';
3214
		}
3215
3216
		$attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3217
3218
		if ( $this->formtype != 'preview' ) {
3219
			$attribs['style'] = 'display: none;';
3220
		}
3221
3222
		$wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3223
3224
		if ( $this->formtype == 'preview' ) {
3225
			$this->showPreview( $previewOutput );
3226
		} else {
3227
			// Empty content container for LivePreview
3228
			$pageViewLang = $this->mTitle->getPageViewLanguage();
3229
			$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3230
				'class' => 'mw-content-' . $pageViewLang->getDir() ];
3231
			$wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3232
		}
3233
3234
		$wgOut->addHTML( '</div>' );
3235
3236 View Code Duplication
		if ( $this->formtype == 'diff' ) {
3237
			try {
3238
				$this->showDiff();
3239
			} catch ( MWContentSerializationException $ex ) {
3240
				$msg = wfMessage(
3241
					'content-failed-to-parse',
3242
					$this->contentModel,
3243
					$this->contentFormat,
3244
					$ex->getMessage()
3245
				);
3246
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3247
			}
3248
		}
3249
	}
3250
3251
	/**
3252
	 * Append preview output to $wgOut.
3253
	 * Includes category rendering if this is a category page.
3254
	 *
3255
	 * @param string $text The HTML to be output for the preview.
3256
	 */
3257
	protected function showPreview( $text ) {
3258
		global $wgOut;
3259
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3260
			$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...
3261
		}
3262
		# This hook seems slightly odd here, but makes things more
3263
		# consistent for extensions.
3264
		Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3265
		$wgOut->addHTML( $text );
3266
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3267
			$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...
3268
		}
3269
	}
3270
3271
	/**
3272
	 * Get a diff between the current contents of the edit box and the
3273
	 * version of the page we're editing from.
3274
	 *
3275
	 * If this is a section edit, we'll replace the section as for final
3276
	 * save and then make a comparison.
3277
	 */
3278
	function showDiff() {
3279
		global $wgUser, $wgContLang, $wgOut;
3280
3281
		$oldtitlemsg = 'currentrev';
3282
		# if message does not exist, show diff against the preloaded default
3283
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3284
			$oldtext = $this->mTitle->getDefaultMessageText();
3285
			if ( $oldtext !== false ) {
3286
				$oldtitlemsg = 'defaultmessagetext';
3287
				$oldContent = $this->toEditContent( $oldtext );
3288
			} else {
3289
				$oldContent = null;
3290
			}
3291
		} else {
3292
			$oldContent = $this->getCurrentContent();
3293
		}
3294
3295
		$textboxContent = $this->toEditContent( $this->textbox1 );
3296
		if ( $this->editRevId !== null ) {
3297
			$newContent = $this->page->replaceSectionAtRev(
3298
				$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 3295 can be null; however, WikiPage::replaceSectionAtRev() 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...
3299
			);
3300
		} else {
3301
			$newContent = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3302
				$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 3295 can be null; however, WikiPage::replaceSectionContent() 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...
3303
			);
3304
		}
3305
3306
		if ( $newContent ) {
3307
			ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3308
			Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3309
3310
			$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3311
			$newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3312
		}
3313
3314
		if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3315
			$oldtitle = wfMessage( $oldtitlemsg )->parse();
3316
			$newtitle = wfMessage( 'yourtext' )->parse();
3317
3318
			if ( !$oldContent ) {
3319
				$oldContent = $newContent->getContentHandler()->makeEmptyContent();
3320
			}
3321
3322
			if ( !$newContent ) {
3323
				$newContent = $oldContent->getContentHandler()->makeEmptyContent();
3324
			}
3325
3326
			$de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3327
			$de->setContent( $oldContent, $newContent );
3328
3329
			$difftext = $de->getDiff( $oldtitle, $newtitle );
3330
			$de->showDiffStyle();
3331
		} else {
3332
			$difftext = '';
3333
		}
3334
3335
		$wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3336
	}
3337
3338
	/**
3339
	 * Show the header copyright warning.
3340
	 */
3341
	protected function showHeaderCopyrightWarning() {
3342
		$msg = 'editpage-head-copy-warn';
3343
		if ( !wfMessage( $msg )->isDisabled() ) {
3344
			global $wgOut;
3345
			$wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3346
				'editpage-head-copy-warn' );
3347
		}
3348
	}
3349
3350
	/**
3351
	 * Give a chance for site and per-namespace customizations of
3352
	 * terms of service summary link that might exist separately
3353
	 * from the copyright notice.
3354
	 *
3355
	 * This will display between the save button and the edit tools,
3356
	 * so should remain short!
3357
	 */
3358
	protected function showTosSummary() {
3359
		$msg = 'editpage-tos-summary';
3360
		Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3361
		if ( !wfMessage( $msg )->isDisabled() ) {
3362
			global $wgOut;
3363
			$wgOut->addHTML( '<div class="mw-tos-summary">' );
3364
			$wgOut->addWikiMsg( $msg );
3365
			$wgOut->addHTML( '</div>' );
3366
		}
3367
	}
3368
3369
	protected function showEditTools() {
3370
		global $wgOut;
3371
		$wgOut->addHTML( '<div class="mw-editTools">' .
3372
			wfMessage( 'edittools' )->inContentLanguage()->parse() .
3373
			'</div>' );
3374
	}
3375
3376
	/**
3377
	 * Get the copyright warning
3378
	 *
3379
	 * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3380
	 * @return string
3381
	 */
3382
	protected function getCopywarn() {
3383
		return self::getCopyrightWarning( $this->mTitle );
3384
	}
3385
3386
	/**
3387
	 * Get the copyright warning, by default returns wikitext
3388
	 *
3389
	 * @param Title $title
3390
	 * @param string $format Output format, valid values are any function of a Message object
3391
	 * @return string
3392
	 */
3393
	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3394
		global $wgRightsText;
3395
		if ( $wgRightsText ) {
3396
			$copywarnMsg = [ 'copyrightwarning',
3397
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3398
				$wgRightsText ];
3399
		} else {
3400
			$copywarnMsg = [ 'copyrightwarning2',
3401
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3402
		}
3403
		// Allow for site and per-namespace customization of contribution/copyright notice.
3404
		Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3405
3406
		return "<div id=\"editpage-copywarn\">\n" .
3407
			call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3408
	}
3409
3410
	/**
3411
	 * Get the Limit report for page previews
3412
	 *
3413
	 * @since 1.22
3414
	 * @param ParserOutput $output ParserOutput object from the parse
3415
	 * @return string HTML
3416
	 */
3417
	public static function getPreviewLimitReport( $output ) {
3418
		if ( !$output || !$output->getLimitReportData() ) {
3419
			return '';
3420
		}
3421
3422
		$limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3423
			wfMessage( 'limitreport-title' )->parseAsBlock()
3424
		);
3425
3426
		// Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3427
		$limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3428
3429
		$limitReport .= Html::openElement( 'table', [
3430
			'class' => 'preview-limit-report wikitable'
3431
		] ) .
3432
			Html::openElement( 'tbody' );
3433
3434
		foreach ( $output->getLimitReportData() as $key => $value ) {
3435
			if ( Hooks::run( 'ParserLimitReportFormat',
3436
				[ $key, &$value, &$limitReport, true, true ]
3437
			) ) {
3438
				$keyMsg = wfMessage( $key );
3439
				$valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3440
				if ( !$valueMsg->exists() ) {
3441
					$valueMsg = new RawMessage( '$1' );
3442
				}
3443
				if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3444
					$limitReport .= Html::openElement( 'tr' ) .
3445
						Html::rawElement( 'th', null, $keyMsg->parse() ) .
3446
						Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3447
						Html::closeElement( 'tr' );
3448
				}
3449
			}
3450
		}
3451
3452
		$limitReport .= Html::closeElement( 'tbody' ) .
3453
			Html::closeElement( 'table' ) .
3454
			Html::closeElement( 'div' );
3455
3456
		return $limitReport;
3457
	}
3458
3459
	protected function showStandardInputs( &$tabindex = 2 ) {
3460
		global $wgOut;
3461
		$wgOut->addHTML( "<div class='editOptions'>\n" );
3462
3463 View Code Duplication
		if ( $this->section != 'new' ) {
3464
			$this->showSummaryInput( false, $this->summary );
3465
			$wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3466
		}
3467
3468
		$checkboxes = $this->getCheckboxes( $tabindex,
3469
			[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3470
		$wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3471
3472
		// Show copyright warning.
3473
		$wgOut->addWikiText( $this->getCopywarn() );
3474
		$wgOut->addHTML( $this->editFormTextAfterWarn );
3475
3476
		$wgOut->addHTML( "<div class='editButtons'>\n" );
3477
		$wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3478
3479
		$cancel = $this->getCancelLink();
3480
		if ( $cancel !== '' ) {
3481
			$cancel .= Html::element( 'span',
3482
				[ 'class' => 'mw-editButtons-pipe-separator' ],
3483
				wfMessage( 'pipe-separator' )->text() );
3484
		}
3485
3486
		$message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3487
		$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3488
		$attrs = [
3489
			'target' => 'helpwindow',
3490
			'href' => $edithelpurl,
3491
		];
3492
		$edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3493
			$attrs, [ 'mw-ui-quiet' ] ) .
3494
			wfMessage( 'word-separator' )->escaped() .
3495
			wfMessage( 'newwindow' )->parse();
3496
3497
		$wgOut->addHTML( "	<span class='cancelLink'>{$cancel}</span>\n" );
3498
		$wgOut->addHTML( "	<span class='editHelp'>{$edithelp}</span>\n" );
3499
		$wgOut->addHTML( "</div><!-- editButtons -->\n" );
3500
3501
		Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3502
3503
		$wgOut->addHTML( "</div><!-- editOptions -->\n" );
3504
	}
3505
3506
	/**
3507
	 * Show an edit conflict. textbox1 is already shown in showEditForm().
3508
	 * If you want to use another entry point to this function, be careful.
3509
	 */
3510
	protected function showConflict() {
3511
		global $wgOut;
3512
3513
		if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3514
			$stats = $wgOut->getContext()->getStats();
3515
			$stats->increment( 'edit.failures.conflict' );
3516
			if ( $this->mTitle->isTalkPage() ) {
3517
				$stats->increment( 'edit.failures.conflict.byType.talk' );
3518
			} else {
3519
				$stats->increment( 'edit.failures.conflict.byType.subject' );
3520
			}
3521
			if ( $this->mTitle->getNamespace() === NS_PROJECT ) {
3522
				$stats->increment( 'edit.failures.conflict.byNamespace.project' );
3523
			}
3524
3525
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3526
3527
			$content1 = $this->toEditContent( $this->textbox1 );
3528
			$content2 = $this->toEditContent( $this->textbox2 );
3529
3530
			$handler = ContentHandler::getForModelID( $this->contentModel );
3531
			$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3532
			$de->setContent( $content2, $content1 );
0 ignored issues
show
Bug introduced by
It seems like $content2 defined by $this->toEditContent($this->textbox2) on line 3528 can be null; however, DifferenceEngine::setContent() 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...
Bug introduced by
It seems like $content1 defined by $this->toEditContent($this->textbox1) on line 3527 can be null; however, DifferenceEngine::setContent() 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...
3533
			$de->showDiff(
3534
				wfMessage( 'yourtext' )->parse(),
3535
				wfMessage( 'storedversion' )->text()
3536
			);
3537
3538
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3539
			$this->showTextbox2();
3540
		}
3541
	}
3542
3543
	/**
3544
	 * @return string
3545
	 */
3546
	public function getCancelLink() {
3547
		$cancelParams = [];
3548
		if ( !$this->isConflict && $this->oldid > 0 ) {
3549
			$cancelParams['oldid'] = $this->oldid;
3550
		} elseif ( $this->getContextTitle()->isRedirect() ) {
3551
			$cancelParams['redirect'] = 'no';
3552
		}
3553
		$attrs = [ 'id' => 'mw-editform-cancel' ];
3554
3555
		return Linker::linkKnown(
3556
			$this->getContextTitle(),
3557
			wfMessage( 'cancel' )->parse(),
3558
			Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3559
			$cancelParams
3560
		);
3561
	}
3562
3563
	/**
3564
	 * Returns the URL to use in the form's action attribute.
3565
	 * This is used by EditPage subclasses when simply customizing the action
3566
	 * variable in the constructor is not enough. This can be used when the
3567
	 * EditPage lives inside of a Special page rather than a custom page action.
3568
	 *
3569
	 * @param Title $title Title object for which is being edited (where we go to for &action= links)
3570
	 * @return string
3571
	 */
3572
	protected function getActionURL( Title $title ) {
3573
		return $title->getLocalURL( [ 'action' => $this->action ] );
3574
	}
3575
3576
	/**
3577
	 * Check if a page was deleted while the user was editing it, before submit.
3578
	 * Note that we rely on the logging table, which hasn't been always there,
3579
	 * but that doesn't matter, because this only applies to brand new
3580
	 * deletes.
3581
	 * @return bool
3582
	 */
3583
	protected function wasDeletedSinceLastEdit() {
3584
		if ( $this->deletedSinceEdit !== null ) {
3585
			return $this->deletedSinceEdit;
3586
		}
3587
3588
		$this->deletedSinceEdit = false;
3589
3590
		if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3591
			$this->lastDelete = $this->getLastDelete();
3592
			if ( $this->lastDelete ) {
3593
				$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3594
				if ( $deleteTime > $this->starttime ) {
3595
					$this->deletedSinceEdit = true;
3596
				}
3597
			}
3598
		}
3599
3600
		return $this->deletedSinceEdit;
3601
	}
3602
3603
	/**
3604
	 * @return bool|stdClass
3605
	 */
3606
	protected function getLastDelete() {
3607
		$dbr = wfGetDB( DB_SLAVE );
3608
		$data = $dbr->selectRow(
3609
			[ 'logging', 'user' ],
3610
			[
3611
				'log_type',
3612
				'log_action',
3613
				'log_timestamp',
3614
				'log_user',
3615
				'log_namespace',
3616
				'log_title',
3617
				'log_comment',
3618
				'log_params',
3619
				'log_deleted',
3620
				'user_name'
3621
			], [
3622
				'log_namespace' => $this->mTitle->getNamespace(),
3623
				'log_title' => $this->mTitle->getDBkey(),
3624
				'log_type' => 'delete',
3625
				'log_action' => 'delete',
3626
				'user_id=log_user'
3627
			],
3628
			__METHOD__,
3629
			[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3630
		);
3631
		// Quick paranoid permission checks...
3632
		if ( is_object( $data ) ) {
3633
			if ( $data->log_deleted & LogPage::DELETED_USER ) {
3634
				$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3635
			}
3636
3637
			if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3638
				$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3639
			}
3640
		}
3641
3642
		return $data;
3643
	}
3644
3645
	/**
3646
	 * Get the rendered text for previewing.
3647
	 * @throws MWException
3648
	 * @return string
3649
	 */
3650
	function getPreviewText() {
3651
		global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3652
		global $wgAllowUserCss, $wgAllowUserJs;
3653
3654
		$stats = $wgOut->getContext()->getStats();
3655
3656
		if ( $wgRawHtml && !$this->mTokenOk ) {
3657
			// Could be an offsite preview attempt. This is very unsafe if
3658
			// HTML is enabled, as it could be an attack.
3659
			$parsedNote = '';
3660
			if ( $this->textbox1 !== '' ) {
3661
				// Do not put big scary notice, if previewing the empty
3662
				// string, which happens when you initially edit
3663
				// a category page, due to automatic preview-on-open.
3664
				$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3665
					wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3666
			}
3667
			$stats->increment( 'edit.failures.session_loss' );
3668
			return $parsedNote;
3669
		}
3670
3671
		$note = '';
3672
3673
		try {
3674
			$content = $this->toEditContent( $this->textbox1 );
3675
3676
			$previewHTML = '';
3677
			if ( !Hooks::run(
3678
				'AlternateEditPreview',
3679
				[ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3680
			) {
3681
				return $previewHTML;
3682
			}
3683
3684
			# provide a anchor link to the editform
3685
			$continueEditing = '<span class="mw-continue-editing">' .
3686
				'[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3687
				wfMessage( 'continue-editing' )->text() . ']]</span>';
3688
			if ( $this->mTriedSave && !$this->mTokenOk ) {
3689
				if ( $this->mTokenOkExceptSuffix ) {
3690
					$note = wfMessage( 'token_suffix_mismatch' )->plain();
3691
					$stats->increment( 'edit.failures.bad_token' );
3692
				} else {
3693
					$note = wfMessage( 'session_fail_preview' )->plain();
3694
					$stats->increment( 'edit.failures.session_loss' );
3695
				}
3696
			} elseif ( $this->incompleteForm ) {
3697
				$note = wfMessage( 'edit_form_incomplete' )->plain();
3698
				if ( $this->mTriedSave ) {
3699
					$stats->increment( 'edit.failures.incomplete_form' );
3700
				}
3701
			} else {
3702
				$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3703
			}
3704
3705
			$parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3706
			$parserOptions->setIsPreview( true );
3707
			$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3708
3709
			# don't parse non-wikitext pages, show message about preview
3710
			if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3711
				if ( $this->mTitle->isCssJsSubpage() ) {
3712
					$level = 'user';
3713
				} elseif ( $this->mTitle->isCssOrJsPage() ) {
3714
					$level = 'site';
3715
				} else {
3716
					$level = false;
3717
				}
3718
3719
				if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3720
					$format = 'css';
3721
					if ( $level === 'user' && !$wgAllowUserCss ) {
3722
						$format = false;
3723
					}
3724
				} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3725
					$format = 'js';
3726
					if ( $level === 'user' && !$wgAllowUserJs ) {
3727
						$format = false;
3728
					}
3729
				} else {
3730
					$format = false;
3731
				}
3732
3733
				# Used messages to make sure grep find them:
3734
				# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3735
				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...
3736
					$note = "<div id='mw-{$level}{$format}preview'>" .
3737
						wfMessage( "{$level}{$format}preview" )->text() .
3738
						' ' . $continueEditing . "</div>";
3739
				}
3740
			}
3741
3742
			# If we're adding a comment, we need to show the
3743
			# summary as the headline
3744
			if ( $this->section === "new" && $this->summary !== "" ) {
3745
				$content = $content->addSectionHeader( $this->summary );
3746
			}
3747
3748
			$hook_args = [ $this, &$content ];
3749
			ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3750
			Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3751
3752
			$parserOptions->enableLimitReport();
3753
3754
			# For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3755
			# But it's now deprecated, so never mind
3756
3757
			$pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3758
			$scopedCallback = $parserOptions->setupFakeRevision(
3759
				$this->mTitle, $pstContent, $wgUser );
3760
			$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3761
3762
			$parserOutput->setEditSectionTokens( false ); // no section edit links
3763
			$previewHTML = $parserOutput->getText();
3764
			$this->mParserOutput = $parserOutput;
3765
			$wgOut->addParserOutputMetadata( $parserOutput );
3766
3767
			if ( count( $parserOutput->getWarnings() ) ) {
3768
				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3769
			}
3770
3771
			ScopedCallback::consume( $scopedCallback );
3772
		} catch ( MWContentSerializationException $ex ) {
3773
			$m = wfMessage(
3774
				'content-failed-to-parse',
3775
				$this->contentModel,
3776
				$this->contentFormat,
3777
				$ex->getMessage()
3778
			);
3779
			$note .= "\n\n" . $m->parse();
3780
			$previewHTML = '';
3781
		}
3782
3783
		if ( $this->isConflict ) {
3784
			$conflict = '<h2 id="mw-previewconflict">'
3785
				. wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3786
		} else {
3787
			$conflict = '<hr />';
3788
		}
3789
3790
		$previewhead = "<div class='previewnote'>\n" .
3791
			'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3792
			$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3793
3794
		$pageViewLang = $this->mTitle->getPageViewLanguage();
3795
		$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3796
			'class' => 'mw-content-' . $pageViewLang->getDir() ];
3797
		$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3798
3799
		return $previewhead . $previewHTML . $this->previewTextAfterContent;
3800
	}
3801
3802
	/**
3803
	 * @return array
3804
	 */
3805
	function getTemplates() {
3806
		if ( $this->preview || $this->section != '' ) {
3807
			$templates = [];
3808
			if ( !isset( $this->mParserOutput ) ) {
3809
				return $templates;
3810
			}
3811
			foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3812
				foreach ( array_keys( $template ) as $dbk ) {
3813
					$templates[] = Title::makeTitle( $ns, $dbk );
3814
				}
3815
			}
3816
			return $templates;
3817
		} else {
3818
			return $this->mTitle->getTemplateLinksFrom();
3819
		}
3820
	}
3821
3822
	/**
3823
	 * Shows a bulletin board style toolbar for common editing functions.
3824
	 * It can be disabled in the user preferences.
3825
	 *
3826
	 * @param Title $title Title object for the page being edited (optional)
3827
	 * @return string
3828
	 */
3829
	static function getEditToolbar( $title = null ) {
3830
		global $wgContLang, $wgOut;
3831
		global $wgEnableUploads, $wgForeignFileRepos;
3832
3833
		$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3834
		$showSignature = true;
3835
		if ( $title ) {
3836
			$showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3837
		}
3838
3839
		/**
3840
		 * $toolarray is an array of arrays each of which includes the
3841
		 * opening tag, the closing tag, optionally a sample text that is
3842
		 * inserted between the two when no selection is highlighted
3843
		 * and.  The tip text is shown when the user moves the mouse
3844
		 * over the button.
3845
		 *
3846
		 * Images are defined in ResourceLoaderEditToolbarModule.
3847
		 */
3848
		$toolarray = [
3849
			[
3850
				'id'     => 'mw-editbutton-bold',
3851
				'open'   => '\'\'\'',
3852
				'close'  => '\'\'\'',
3853
				'sample' => wfMessage( 'bold_sample' )->text(),
3854
				'tip'    => wfMessage( 'bold_tip' )->text(),
3855
			],
3856
			[
3857
				'id'     => 'mw-editbutton-italic',
3858
				'open'   => '\'\'',
3859
				'close'  => '\'\'',
3860
				'sample' => wfMessage( 'italic_sample' )->text(),
3861
				'tip'    => wfMessage( 'italic_tip' )->text(),
3862
			],
3863
			[
3864
				'id'     => 'mw-editbutton-link',
3865
				'open'   => '[[',
3866
				'close'  => ']]',
3867
				'sample' => wfMessage( 'link_sample' )->text(),
3868
				'tip'    => wfMessage( 'link_tip' )->text(),
3869
			],
3870
			[
3871
				'id'     => 'mw-editbutton-extlink',
3872
				'open'   => '[',
3873
				'close'  => ']',
3874
				'sample' => wfMessage( 'extlink_sample' )->text(),
3875
				'tip'    => wfMessage( 'extlink_tip' )->text(),
3876
			],
3877
			[
3878
				'id'     => 'mw-editbutton-headline',
3879
				'open'   => "\n== ",
3880
				'close'  => " ==\n",
3881
				'sample' => wfMessage( 'headline_sample' )->text(),
3882
				'tip'    => wfMessage( 'headline_tip' )->text(),
3883
			],
3884
			$imagesAvailable ? [
3885
				'id'     => 'mw-editbutton-image',
3886
				'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3887
				'close'  => ']]',
3888
				'sample' => wfMessage( 'image_sample' )->text(),
3889
				'tip'    => wfMessage( 'image_tip' )->text(),
3890
			] : false,
3891
			$imagesAvailable ? [
3892
				'id'     => 'mw-editbutton-media',
3893
				'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3894
				'close'  => ']]',
3895
				'sample' => wfMessage( 'media_sample' )->text(),
3896
				'tip'    => wfMessage( 'media_tip' )->text(),
3897
			] : false,
3898
			[
3899
				'id'     => 'mw-editbutton-nowiki',
3900
				'open'   => "<nowiki>",
3901
				'close'  => "</nowiki>",
3902
				'sample' => wfMessage( 'nowiki_sample' )->text(),
3903
				'tip'    => wfMessage( 'nowiki_tip' )->text(),
3904
			],
3905
			$showSignature ? [
3906
				'id'     => 'mw-editbutton-signature',
3907
				'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
3908
				'close'  => '',
3909
				'sample' => '',
3910
				'tip'    => wfMessage( 'sig_tip' )->text(),
3911
			] : false,
3912
			[
3913
				'id'     => 'mw-editbutton-hr',
3914
				'open'   => "\n----\n",
3915
				'close'  => '',
3916
				'sample' => '',
3917
				'tip'    => wfMessage( 'hr_tip' )->text(),
3918
			]
3919
		];
3920
3921
		$script = 'mw.loader.using("mediawiki.toolbar", function () {';
3922
		foreach ( $toolarray as $tool ) {
3923
			if ( !$tool ) {
3924
				continue;
3925
			}
3926
3927
			$params = [
3928
				// Images are defined in ResourceLoaderEditToolbarModule
3929
				false,
3930
				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3931
				// Older browsers show a "speedtip" type message only for ALT.
3932
				// Ideally these should be different, realistically they
3933
				// probably don't need to be.
3934
				$tool['tip'],
3935
				$tool['open'],
3936
				$tool['close'],
3937
				$tool['sample'],
3938
				$tool['id'],
3939
			];
3940
3941
			$script .= Xml::encodeJsCall(
3942
				'mw.toolbar.addButton',
3943
				$params,
3944
				ResourceLoader::inDebugMode()
3945
			);
3946
		}
3947
3948
		$script .= '});';
3949
		$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3950
3951
		$toolbar = '<div id="toolbar"></div>';
3952
3953
		Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3954
3955
		return $toolbar;
3956
	}
3957
3958
	/**
3959
	 * Returns an array of html code of the following checkboxes:
3960
	 * minor and watch
3961
	 *
3962
	 * @param int $tabindex Current tabindex
3963
	 * @param array $checked Array of checkbox => bool, where bool indicates the checked
3964
	 *                 status of the checkbox
3965
	 *
3966
	 * @return array
3967
	 */
3968
	public function getCheckboxes( &$tabindex, $checked ) {
3969
		global $wgUser, $wgUseMediaWikiUIEverywhere;
3970
3971
		$checkboxes = [];
3972
3973
		// don't show the minor edit checkbox if it's a new page or section
3974
		if ( !$this->isNew ) {
3975
			$checkboxes['minor'] = '';
3976
			$minorLabel = wfMessage( 'minoredit' )->parse();
3977 View Code Duplication
			if ( $wgUser->isAllowed( 'minoredit' ) ) {
3978
				$attribs = [
3979
					'tabindex' => ++$tabindex,
3980
					'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3981
					'id' => 'wpMinoredit',
3982
				];
3983
				$minorEditHtml =
3984
					Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3985
					"&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3986
					Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3987
					">{$minorLabel}</label>";
3988
3989
				if ( $wgUseMediaWikiUIEverywhere ) {
3990
					$checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3991
						$minorEditHtml .
3992
					Html::closeElement( 'div' );
3993
				} else {
3994
					$checkboxes['minor'] = $minorEditHtml;
3995
				}
3996
			}
3997
		}
3998
3999
		$watchLabel = wfMessage( 'watchthis' )->parse();
4000
		$checkboxes['watch'] = '';
4001 View Code Duplication
		if ( $wgUser->isLoggedIn() ) {
4002
			$attribs = [
4003
				'tabindex' => ++$tabindex,
4004
				'accesskey' => wfMessage( 'accesskey-watch' )->text(),
4005
				'id' => 'wpWatchthis',
4006
			];
4007
			$watchThisHtml =
4008
				Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
4009
				"&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
4010
				Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
4011
				">{$watchLabel}</label>";
4012
			if ( $wgUseMediaWikiUIEverywhere ) {
4013
				$checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4014
					$watchThisHtml .
4015
					Html::closeElement( 'div' );
4016
			} else {
4017
				$checkboxes['watch'] = $watchThisHtml;
4018
			}
4019
		}
4020
		Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
4021
		return $checkboxes;
4022
	}
4023
4024
	/**
4025
	 * Returns an array of html code of the following buttons:
4026
	 * save, diff, preview and live
4027
	 *
4028
	 * @param int $tabindex Current tabindex
4029
	 *
4030
	 * @return array
4031
	 */
4032
	public function getEditButtons( &$tabindex ) {
4033
		$buttons = [];
4034
4035
		$attribs = [
4036
			'id' => 'wpSave',
4037
			'name' => 'wpSave',
4038
			'tabindex' => ++$tabindex,
4039
		] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4040
		$buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
4041
			$attribs, [ 'mw-ui-constructive' ] );
4042
4043
		++$tabindex; // use the same for preview and live preview
4044
		$attribs = [
4045
			'id' => 'wpPreview',
4046
			'name' => 'wpPreview',
4047
			'tabindex' => $tabindex,
4048
		] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4049
		$buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4050
			$attribs );
4051
		$buttons['live'] = '';
4052
4053
		$attribs = [
4054
			'id' => 'wpDiff',
4055
			'name' => 'wpDiff',
4056
			'tabindex' => ++$tabindex,
4057
		] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4058
		$buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4059
			$attribs );
4060
4061
		Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4062
		return $buttons;
4063
	}
4064
4065
	/**
4066
	 * Creates a basic error page which informs the user that
4067
	 * they have attempted to edit a nonexistent section.
4068
	 */
4069
	function noSuchSectionPage() {
4070
		global $wgOut;
4071
4072
		$wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4073
4074
		$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4075
		Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4076
		$wgOut->addHTML( $res );
4077
4078
		$wgOut->returnToMain( false, $this->mTitle );
4079
	}
4080
4081
	/**
4082
	 * Show "your edit contains spam" page with your diff and text
4083
	 *
4084
	 * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
4085
	 */
4086
	public function spamPageWithContent( $match = false ) {
4087
		global $wgOut, $wgLang;
4088
		$this->textbox2 = $this->textbox1;
4089
4090
		if ( is_array( $match ) ) {
4091
			$match = $wgLang->listToText( $match );
4092
		}
4093
		$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4094
4095
		$wgOut->addHTML( '<div id="spamprotected">' );
4096
		$wgOut->addWikiMsg( 'spamprotectiontext' );
4097
		if ( $match ) {
4098
			$wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4099
		}
4100
		$wgOut->addHTML( '</div>' );
4101
4102
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4103
		$this->showDiff();
4104
4105
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4106
		$this->showTextbox2();
4107
4108
		$wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4109
	}
4110
4111
	/**
4112
	 * Check if the browser is on a blacklist of user-agents known to
4113
	 * mangle UTF-8 data on form submission. Returns true if Unicode
4114
	 * should make it through, false if it's known to be a problem.
4115
	 * @return bool
4116
	 */
4117
	private function checkUnicodeCompliantBrowser() {
4118
		global $wgBrowserBlackList, $wgRequest;
4119
4120
		$currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4121
		if ( $currentbrowser === false ) {
4122
			// No User-Agent header sent? Trust it by default...
4123
			return true;
4124
		}
4125
4126
		foreach ( $wgBrowserBlackList as $browser ) {
4127
			if ( preg_match( $browser, $currentbrowser ) ) {
4128
				return false;
4129
			}
4130
		}
4131
		return true;
4132
	}
4133
4134
	/**
4135
	 * Filter an input field through a Unicode de-armoring process if it
4136
	 * came from an old browser with known broken Unicode editing issues.
4137
	 *
4138
	 * @param WebRequest $request
4139
	 * @param string $field
4140
	 * @return string
4141
	 */
4142
	protected function safeUnicodeInput( $request, $field ) {
4143
		$text = rtrim( $request->getText( $field ) );
4144
		return $request->getBool( 'safemode' )
4145
			? $this->unmakeSafe( $text )
4146
			: $text;
4147
	}
4148
4149
	/**
4150
	 * Filter an output field through a Unicode armoring process if it is
4151
	 * going to an old browser with known broken Unicode editing issues.
4152
	 *
4153
	 * @param string $text
4154
	 * @return string
4155
	 */
4156
	protected function safeUnicodeOutput( $text ) {
4157
		global $wgContLang;
4158
		$codedText = $wgContLang->recodeForEdit( $text );
4159
		return $this->checkUnicodeCompliantBrowser()
4160
			? $codedText
4161
			: $this->makeSafe( $codedText );
4162
	}
4163
4164
	/**
4165
	 * A number of web browsers are known to corrupt non-ASCII characters
4166
	 * in a UTF-8 text editing environment. To protect against this,
4167
	 * detected browsers will be served an armored version of the text,
4168
	 * with non-ASCII chars converted to numeric HTML character references.
4169
	 *
4170
	 * Preexisting such character references will have a 0 added to them
4171
	 * to ensure that round-trips do not alter the original data.
4172
	 *
4173
	 * @param string $invalue
4174
	 * @return string
4175
	 */
4176
	private function makeSafe( $invalue ) {
4177
		// Armor existing references for reversibility.
4178
		$invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4179
4180
		$bytesleft = 0;
4181
		$result = "";
4182
		$working = 0;
4183
		$valueLength = strlen( $invalue );
4184
		for ( $i = 0; $i < $valueLength; $i++ ) {
4185
			$bytevalue = ord( $invalue[$i] );
4186
			if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4187
				$result .= chr( $bytevalue );
4188
				$bytesleft = 0;
4189
			} elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4190
				$working = $working << 6;
4191
				$working += ( $bytevalue & 0x3F );
4192
				$bytesleft--;
4193
				if ( $bytesleft <= 0 ) {
4194
					$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4195
				}
4196
			} elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4197
				$working = $bytevalue & 0x1F;
4198
				$bytesleft = 1;
4199
			} elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4200
				$working = $bytevalue & 0x0F;
4201
				$bytesleft = 2;
4202
			} else { // 1111 0xxx
4203
				$working = $bytevalue & 0x07;
4204
				$bytesleft = 3;
4205
			}
4206
		}
4207
		return $result;
4208
	}
4209
4210
	/**
4211
	 * Reverse the previously applied transliteration of non-ASCII characters
4212
	 * back to UTF-8. Used to protect data from corruption by broken web browsers
4213
	 * as listed in $wgBrowserBlackList.
4214
	 *
4215
	 * @param string $invalue
4216
	 * @return string
4217
	 */
4218
	private function unmakeSafe( $invalue ) {
4219
		$result = "";
4220
		$valueLength = strlen( $invalue );
4221
		for ( $i = 0; $i < $valueLength; $i++ ) {
4222
			if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4223
				$i += 3;
4224
				$hexstring = "";
4225
				do {
4226
					$hexstring .= $invalue[$i];
4227
					$i++;
4228
				} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4229
4230
				// Do some sanity checks. These aren't needed for reversibility,
4231
				// but should help keep the breakage down if the editor
4232
				// breaks one of the entities whilst editing.
4233
				if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4234
					$codepoint = hexdec( $hexstring );
4235
					$result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4236
				} else {
4237
					$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4238
				}
4239
			} else {
4240
				$result .= substr( $invalue, $i, 1 );
4241
			}
4242
		}
4243
		// reverse the transform that we made for reversibility reasons.
4244
		return strtr( $result, [ "&#x0" => "&#x" ] );
4245
	}
4246
}
4247