EditEntityAction   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 57
lcom 1
cbo 4
dl 0
loc 476
rs 5.04
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getName() 0 3 1
A showPermissionError() 0 14 3
A loadRevisions() 0 18 3
C getStatus() 0 70 16
A showUndoErrorPage() 0 10 1
A show() 0 9 4
C showUndoForm() 0 94 13
A getTitleText() 0 5 1
A getCancelLink() 0 9 1
A showDiffStyle() 0 3 1
A getSummaryInput() 0 19 1
A displayUndoDiff() 0 34 1
A getEditButton() 0 13 2
B showConfirmationForm() 0 55 6
A requiresUnblock() 0 3 1
A requiresWrite() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EditEntityAction 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 EditEntityAction, and based on these observations, apply Extract Interface, too.

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