Issues (1401)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

repo/includes/Actions/EditEntityAction.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Wikibase\Repo\Actions;
4
5
use Html;
6
use IContextSource;
7
use Linker;
8
use MediaWiki\MediaWikiServices;
9
use MediaWiki\Revision\RevisionRecord;
10
use MediaWiki\Revision\SlotRecord;
11
use MWException;
12
use OOUI\ButtonInputWidget;
13
use OOUI\ButtonWidget;
14
use OOUI\FieldLayout;
15
use OOUI\HtmlSnippet;
16
use OOUI\TextInputWidget;
17
use Page;
18
use Status;
19
use WebRequest;
20
use Wikibase\Repo\Content\EntityContent;
21
use Wikibase\Repo\Content\EntityContentDiff;
22
use Wikibase\Repo\Diff\BasicEntityDiffVisualizer;
23
use Wikibase\Repo\Diff\DispatchingEntityDiffVisualizer;
24
use Wikibase\Repo\WikibaseRepo;
25
26
/**
27
 * Handles the edit action for Wikibase entities.
28
 * This shows the forms for the undo and restore operations if requested.
29
 * Otherwise it will just show the normal entity view.
30
 *
31
 * @license GPL-2.0-or-later
32
 * @author Jeroen De Dauw < [email protected] >
33
 * @author Jens Ohlig
34
 * @author Daniel Kinzler
35
 */
36
class EditEntityAction extends ViewEntityAction {
37
38
	/**
39
	 * @var BasicEntityDiffVisualizer
40
	 */
41
	private $entityDiffVisualizer;
42
43
	/**
44
	 * @see Action::__construct
45
	 *
46
	 * @param Page $page
47
	 * @param IContextSource|null $context
48
	 */
49
	public function __construct( Page $page, IContextSource $context = null ) {
50
		parent::__construct( $page, $context );
51
52
		$this->entityDiffVisualizer = new DispatchingEntityDiffVisualizer(
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Wikibase\Repo\Diff\...y($this->getContext())) of type object<Wikibase\Repo\Dif...ngEntityDiffVisualizer> is incompatible with the declared type object<Wikibase\Repo\Dif...icEntityDiffVisualizer> of property $entityDiffVisualizer.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
53
			WikibaseRepo::getDefaultInstance()
54
				->getEntityDiffVisualizerFactory( $this->getContext() )
55
		);
56
	}
57
58
	/**
59
	 * @see Action::getName()
60
	 *
61
	 * @return string
62
	 */
63
	public function getName() {
64
		return 'edit';
65
	}
66
67
	/**
68
	 * Show an error page if the user is not allowed to perform the given action.
69
	 *
70
	 * @param string $action The action to check
71
	 *
72
	 * @return bool true if there were permission errors
73
	 */
74
	protected function showPermissionError( $action ) {
75
		$rigor = $this->getRequest()->wasPosted() ? 'secure' : 'full';
76
		$pm = MediaWikiServices::getInstance()->getPermissionManager();
77
		if ( !$pm->userCan( $action, $this->getUser(), $this->getTitle(), $rigor ) ) {
78
			$this->getOutput()->showPermissionsErrorPage(
79
				$pm->getPermissionErrors( $action, $this->getUser(), $this->getTitle(), $rigor ),
80
				$action
81
			);
82
83
			return true;
84
		}
85
86
		return false;
87
	}
88
89
	/**
90
	 * Loads the revisions specified by the web request and returns them as a three element array
91
	 * wrapped in a Status object. If any error arises, it will be reported using the status object.
92
	 *
93
	 * @return Status A Status object containing an array with three revision record objects,
94
	 *   [ $olderRevision, $newerRevision, $latestRevision ].
95
	 * @throws MWException if the page's latest revision cannot be loaded
96
	 */
97
	protected function loadRevisions() {
98
		$latestRevId = $this->getTitle()->getLatestRevID();
99
100
		if ( $latestRevId === 0 ) {
101
			// XXX: Better message
102
			return Status::newFatal( 'missing-article', $this->getTitle()->getPrefixedText(), '' );
103
		}
104
105
		$latestRevision = MediaWikiServices::getInstance()
106
			->getRevisionLookup()
107
			->getRevisionById( $latestRevId );
108
109
		if ( !$latestRevId ) {
110
			throw new MWException( "latest revision not found: $latestRevId" );
111
		}
112
113
		return $this->getStatus( $this->getRequest(), $latestRevision );
114
	}
115
116
	/**
117
	 * @param WebRequest $req
118
	 * @param RevisionRecord $latestRevision
119
	 *
120
	 * @return Status
121
	 */
122
	private function getStatus( WebRequest $req, RevisionRecord $latestRevision ) {
123
		$revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
124
		if ( $req->getCheck( 'restore' ) ) { // nearly the same as undoafter without undo
125
			$olderRevision = $revLookup->getRevisionById( $req->getInt( 'restore' ) );
126
127
			if ( !$olderRevision ) {
128
				return Status::newFatal( 'undo-norev', $req->getInt( 'restore' ) );
129
			}
130
131
			// ignore undo, even if set
132
			$newerRevision = $latestRevision;
133
		} elseif ( $req->getCheck( 'undo' ) ) {
134
			$newerRevision = $revLookup->getRevisionById( $req->getInt( 'undo' ) );
135
136
			if ( !$newerRevision ) {
137
				return Status::newFatal( 'undo-norev', $req->getInt( 'undo' ) );
138
			}
139
140
			if ( $req->getCheck( 'undoafter' ) ) {
141
				$olderRevision = $revLookup->getRevisionById( $req->getInt( 'undoafter' ) );
142
143
				if ( !$olderRevision ) {
144
					return Status::newFatal( 'undo-norev', $req->getInt( 'undoafter' ) );
145
				}
146
			} else {
147
				$olderRevision = $revLookup->getPreviousRevision( $newerRevision );
148
149
				if ( !$olderRevision ) {
150
					return Status::newFatal( 'wikibase-undo-firstrev' );
151
				}
152
			}
153
		} elseif ( $req->getCheck( 'undoafter' ) ) {
154
			$olderRevision = $revLookup->getRevisionById( $req->getInt( 'undoafter' ) );
155
156
			if ( !$olderRevision ) {
157
				return Status::newFatal( 'undo-norev', $req->getInt( 'undo' ) );
158
			}
159
160
			// we already know that undo is not set
161
			$newerRevision = $latestRevision;
162
		} else {
163
			return Status::newFatal( 'edit_form_incomplete' ); //XXX: better message?
164
		}
165
166
		if ( $olderRevision->getId() == $newerRevision->getId() ) {
167
			return Status::newFatal( 'wikibase-undo-samerev', $this->getTitle() );
168
		}
169
170
		if ( $newerRevision->getPageId() != $latestRevision->getPageId() ) {
171
			return Status::newFatal( 'wikibase-undo-badpage', $this->getTitle(), $newerRevision->getId() );
172
		}
173
174
		if ( $olderRevision->getPageId() != $latestRevision->getPageId() ) {
175
			return Status::newFatal( 'wikibase-undo-badpage', $this->getTitle(), $olderRevision->getId() );
176
		}
177
178
		if ( $olderRevision->getContent( SlotRecord::MAIN ) === null ) {
179
			return Status::newFatal( 'wikibase-undo-nocontent', $this->getTitle(), $olderRevision->getId() );
180
		}
181
182
		if ( $newerRevision->getContent( SlotRecord::MAIN ) === null ) {
183
			return Status::newFatal( 'wikibase-undo-nocontent', $this->getTitle(), $newerRevision->getId() );
184
		}
185
186
		if ( $latestRevision->getContent( SlotRecord::MAIN ) === null ) {
187
			return Status::newFatal( 'wikibase-undo-nocontent', $this->getTitle(), $latestRevision->getId() );
188
		}
189
190
		return Status::newGood( [ $olderRevision, $newerRevision, $latestRevision ] );
191
	}
192
193
	/**
194
	 * Output an error page showing the given status
195
	 *
196
	 * @param Status $status The status to report.
197
	 */
198
	protected function showUndoErrorPage( Status $status ) {
199
		$this->getOutput()->prepareErrorPage(
200
			$this->msg( 'wikibase-undo-revision-error' ),
201
			$this->msg( 'errorpagetitle' )
202
		);
203
204
		$this->getOutput()->addHTML( $status->getMessage()->parse() );
205
206
		$this->getOutput()->returnToMain();
207
	}
208
209
	/**
210
	 * @see FormlessAction::show
211
	 *
212
	 * Calls parent show() action to just display the entity, unless an undo action is requested.
213
	 */
214
	public function show() {
215
		$req = $this->getRequest();
216
217
		if ( $req->getCheck( 'undo' ) || $req->getCheck( 'undoafter' ) || $req->getCheck( 'restore' ) ) {
218
			$this->showUndoForm();
219
		} else {
220
			parent::show();
221
		}
222
	}
223
224
	private function showUndoForm() {
225
		$this->getOutput()->enableOOUI();
226
		$req = $this->getRequest();
227
228
		if ( $this->showPermissionError( 'read' ) || $this->showPermissionError( 'edit' ) ) {
229
			return;
230
		}
231
232
		$revisions = $this->loadRevisions();
233
		if ( !$revisions->isOK() ) {
234
			$this->showUndoErrorPage( $revisions );
235
			return;
236
		}
237
238
		/**
239
		 * @var RevisionRecord $olderRevision
240
		 * @var RevisionRecord $newerRevision
241
		 * @var RevisionRecord $latestRevision
242
		 */
243
		list( $olderRevision, $newerRevision, $latestRevision ) = $revisions->getValue();
244
245
		/**
246
		 * @var EntityContent $olderContent
247
		 * @var EntityContent $newerContent
248
		 * @var EntityContent $latestContent
249
		 */
250
		$olderContent = $olderRevision->getContent( SlotRecord::MAIN );
251
		$newerContent = $newerRevision->getContent( SlotRecord::MAIN );
252
		$latestContent = $latestRevision->getContent( SlotRecord::MAIN );
253
254
		$restore = $req->getCheck( 'restore' );
255
256
		$this->getOutput()->setPageTitle(
257
			$this->msg(
258
				$restore ? 'wikibase-restore-title' : 'wikibase-undo-title',
259
				$this->getTitleText(),
260
				$olderRevision->getId(),
261
				$newerRevision->getId()
262
			)
263
		);
264
265
		// diff from newer to older
266
		$diff = $newerContent->getDiff( $olderContent );
267
268
		if ( $newerRevision->getId() == $latestRevision->getId() ) {
269
			// if the revision to undo is the latest revision, then there can be no conflicts
270
			$appDiff = $diff;
271
		} else {
272
			$patchedCurrent = $latestContent->getPatchedCopy( $diff );
273
			$appDiff = $latestContent->getDiff( $patchedCurrent );
274
		}
275
276
		if ( !$restore ) {
277
			$omitted = $diff->count() - $appDiff->count();
278
279
			if ( !$appDiff->isEmpty() ) {
280
				$this->getOutput()->addHTML( Html::openElement( 'p' ) );
281
				$this->getOutput()->addWikiMsg( $omitted > 0 ? 'wikibase-partial-undo' : 'undo-success' );
282
				$this->getOutput()->addHTML( Html::closeElement( 'p' ) );
283
			}
284
285
			if ( $omitted > 0 ) {
286
				$this->getOutput()->addHTML( Html::openElement( 'p' ) );
287
				$this->getOutput()->addWikiMsg( 'wikibase-omitted-undo-ops', $omitted );
288
				$this->getOutput()->addHTML( Html::closeElement( 'p' ) );
289
			}
290
		}
291
292
		if ( $appDiff->isEmpty() ) {
293
			$this->getOutput()->addHTML( Html::openElement( 'p' ) );
294
			$this->getOutput()->addWikiMsg( 'wikibase-empty-undo' );
295
			$this->getOutput()->addHTML( Html::closeElement( 'p' ) );
296
			return;
297
		}
298
299
		if ( $this->getUser()->isAnon() ) {
300
			$this->getOutput()->addHTML( Html::rawElement(
301
				'p',
302
				[ 'class' => 'warning' ],
303
				$this->msg(
304
					'wikibase-anonymouseditwarning',
305
					$this->msg( 'wikibase-entity-item' )->text()
306
				)->parse()
307
			) );
308
		}
309
310
		$this->displayUndoDiff( $appDiff );
311
312
		if ( $restore ) {
313
			$this->showConfirmationForm();
314
		} else {
315
			$this->showConfirmationForm( $newerRevision->getId() );
316
		}
317
	}
318
319
	/**
320
	 * Used for overriding the page HTML title with the label, if available, or else the id.
321
	 * This is passed via parser output and output page to save overhead on view / edit actions.
322
	 *
323
	 * @return string
324
	 */
325
	private function getTitleText() {
326
		$meta = $this->getOutput()->getProperty( 'wikibase-meta-tags' );
327
328
		return $meta['title'] ?? $this->getTitle()->getPrefixedText();
329
	}
330
331
	/**
332
	 * Returns a cancel link back to viewing the entity's page
333
	 *
334
	 * @return string
335
	 */
336
	private function getCancelLink() {
337
		return ( new ButtonWidget( [
338
			'id' => 'mw-editform-cancel',
339
			'href' => $this->getContext()->getTitle()->getLocalURL(),
340
			'label' => $this->msg( 'cancel' )->text(),
341
			'framed' => false,
342
			'flags' => 'destructive'
343
		] ) )->toString();
344
	}
345
346
	/**
347
	 * Add style sheets and supporting JS for diff display.
348
	 */
349
	private function showDiffStyle() {
350
		$this->getOutput()->addModuleStyles( 'mediawiki.diff.styles' );
351
	}
352
353
	/**
354
	 * Generate standard summary input and label (wgSummary), compatible to EditPage.
355
	 *
356
	 * @param string $labelText The html to place inside the label
357
	 *
358
	 * @return string HTML
359
	 */
360
	private function getSummaryInput( $labelText ) {
361
		$inputAttrs = [
362
			'name' => 'wpSummary',
363
			'maxLength' => 200,
364
			'size' => 60,
365
			'spellcheck' => 'true',
366
			'accessKey' => $this->msg( 'accesskey-summary' )->plain(),
367
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
368
369
		return ( new FieldLayout(
370
			new TextInputWidget( $inputAttrs ),
371
			[
372
				'label' => new HtmlSnippet( $labelText ),
373
				'align' => 'top',
374
				'id' => 'wpSummaryLabel',
375
				'classes' => [ 'mw-summary' ],
376
			]
377
		) )->toString();
378
	}
379
380
	private function displayUndoDiff( EntityContentDiff $diff ) {
381
		$tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getTitle()->getPageLanguage()->alignStart() );
382
383
		$this->getOutput()->addHTML( Html::openElement( 'table', [ 'class' => $tableClass ] ) );
384
385
		$this->getOutput()->addHTML( '<colgroup>'
386
			. '<col class="diff-marker"><col class="diff-content">'
387
			. '<col class="diff-marker"><col class="diff-content">'
388
			. '</colgroup>' );
389
		$this->getOutput()->addHTML( Html::openElement( 'tbody' ) );
390
391
		$old = $this->msg( 'currentrev' )->parse();
392
		$new = $this->msg( 'yourtext' )->parse(); //XXX: better message?
393
394
		$this->getOutput()->addHTML( Html::openElement( 'tr', [ 'style' => 'vertical-align: top;' ] ) );
395
		$this->getOutput()->addHTML(
396
			Html::rawElement( 'td', [ 'colspan' => '2' ],
397
				Html::rawElement( 'div', [ 'id' => 'mw-diff-otitle1' ], $old )
398
			)
399
		);
400
		$this->getOutput()->addHTML(
401
			Html::rawElement( 'td', [ 'colspan' => '2' ],
402
				Html::rawElement( 'div', [ 'id' => 'mw-diff-ntitle1' ], $new )
403
			)
404
		);
405
		$this->getOutput()->addHTML( Html::closeElement( 'tr' ) );
406
407
		$this->getOutput()->addHTML( $this->entityDiffVisualizer->visualizeEntityContentDiff( $diff ) );
408
409
		$this->getOutput()->addHTML( Html::closeElement( 'tbody' ) );
410
		$this->getOutput()->addHTML( Html::closeElement( 'table' ) );
411
412
		$this->showDiffStyle();
413
	}
414
415
	/**
416
	 * @return string HTML
417
	 */
418
	private function getEditButton() {
419
		global $wgEditSubmitButtonLabelPublish;
420
		$msgKey = $wgEditSubmitButtonLabelPublish ? 'publishchanges' : 'savearticle';
421
		return ( new ButtonInputWidget( [
422
				'name' => 'wpSave',
423
				'value' => $this->msg( $msgKey )->text(),
424
				'label' => $this->msg( $msgKey )->text(),
425
				'accessKey' => $this->msg( 'accesskey-save' )->plain(),
426
				'flags' => [ 'primary', 'progressive' ],
427
				'type' => 'submit',
428
				'title' => $this->msg( 'tooltip-save' )->text() . ' [' . $this->msg( 'accesskey-save' )->text() . ']',
429
			] ) )->toString();
430
	}
431
432
	/**
433
	 * Shows a form that can be used to confirm the requested undo/restore action.
434
	 *
435
	 * @param int $undidRevision
436
	 */
437
	private function showConfirmationForm( $undidRevision = 0 ) {
438
		$req = $this->getRequest();
439
440
		$args = [
441
			'action' => 'submit',
442
		];
443
444
		if ( $req->getInt( 'undo' ) ) {
445
			$args[ 'undo' ] = $req->getInt( 'undo' );
446
		}
447
448
		if ( $req->getInt( 'undoafter' ) ) {
449
			$args[ 'undoafter' ] = $req->getInt( 'undoafter' );
450
		}
451
452
		if ( $req->getInt( 'restore' ) ) {
453
			$args[ 'restore' ] = $req->getInt( 'restore' );
454
		}
455
456
		$actionUrl = $this->getTitle()->getLocalURL( $args );
457
458
		$this->getOutput()->addHTML( Html::openElement( 'div', [ 'style' => 'margin-top: 1em;' ] ) );
459
460
		$this->getOutput()->addHTML( Html::openElement( 'form', [
461
			'id' => 'undo',
462
			'name' => 'undo',
463
			'method' => 'post',
464
			'action' => $actionUrl,
465
			'enctype' => 'multipart/form-data' ] ) );
466
467
		$this->getOutput()->addHTML( "<div class='editOptions'>\n" );
468
469
		$labelText = $this->msg( 'wikibase-summary-generated' )->escaped();
470
		$this->getOutput()->addHTML( $this->getSummaryInput( $labelText ) );
471
		$this->getOutput()->addHTML( Html::rawElement( 'br' ) );
472
		$this->getOutput()->addHTML( "<div class='editButtons'>\n" );
473
		$this->getOutput()->addHTML( $this->getEditButton() . "\n" );
474
		$this->getOutput()->addHTML( $this->getCancelLink() );
475
476
		$this->getOutput()->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" );
477
478
		$hidden = [
479
			'wpEditToken' => $this->getUser()->getEditToken(),
480
			'wpBaseRev' => $this->getTitle()->getLatestRevID(),
481
		];
482
		if ( !empty( $undidRevision ) ) {
483
			$hidden['wpUndidRevision'] = $undidRevision;
484
		}
485
		foreach ( $hidden as $name => $value ) {
486
			$this->getOutput()->addHTML( "\n" . Html::hidden( $name, $value ) . "\n" );
487
		}
488
489
		$this->getOutput()->addHTML( Html::closeElement( 'form' ) );
490
		$this->getOutput()->addHTML( Html::closeElement( 'div' ) );
491
	}
492
493
	/**
494
	 * @see Action::requiresUnblock
495
	 *
496
	 * @return bool Always true.
497
	 */
498
	public function requiresUnblock() {
499
		return true;
500
	}
501
502
	/**
503
	 * @see Action::requiresWrite
504
	 *
505
	 * @return bool Always true.
506
	 */
507
	public function requiresWrite() {
508
		return true;
509
	}
510
511
}
512