This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
|
|||
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 |
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..