Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 364 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialRevisionDelete | |
0.00% |
0 / 363 |
|
0.00% |
0 / 16 |
4830 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 90 |
|
0.00% |
0 / 1 |
342 | |||
showConvenienceLinks | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
getLogQueryCond | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
tryShowFile | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
30 | |||
getList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
showForm | |
0.00% |
0 / 99 |
|
0.00% |
0 / 1 |
110 | |||
addUsageText | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
buildCheckBoxes | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
56 | |||
submit | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
success | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
failure | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
extractBitParams | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
save | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Implements Special:Revisiondelete |
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 | * @ingroup SpecialPage |
22 | */ |
23 | |
24 | namespace MediaWiki\Specials; |
25 | |
26 | use ErrorPageError; |
27 | use File; |
28 | use LogEventsList; |
29 | use LogPage; |
30 | use MediaWiki\CommentStore\CommentStore; |
31 | use MediaWiki\Html\Html; |
32 | use MediaWiki\HTMLForm\HTMLForm; |
33 | use MediaWiki\Permissions\PermissionManager; |
34 | use MediaWiki\Revision\RevisionRecord; |
35 | use MediaWiki\SpecialPage\SpecialPage; |
36 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\Title\Title; |
39 | use PermissionsError; |
40 | use RepoGroup; |
41 | use RevDelList; |
42 | use RevisionDeleter; |
43 | use UserBlockedError; |
44 | use Xml; |
45 | |
46 | /** |
47 | * Special page allowing users with the appropriate permissions to view |
48 | * and hide revisions. Log items can also be hidden. |
49 | * |
50 | * @ingroup SpecialPage |
51 | */ |
52 | class SpecialRevisionDelete extends UnlistedSpecialPage { |
53 | /** @var bool Was the DB modified in this request */ |
54 | protected $wasSaved = false; |
55 | |
56 | /** @var bool True if the submit button was clicked, and the form was posted */ |
57 | private $submitClicked; |
58 | |
59 | /** @var array Target ID list */ |
60 | private $ids; |
61 | |
62 | /** @var string Archive name, for reviewing deleted files */ |
63 | private $archiveName; |
64 | |
65 | /** @var string Edit token for securing image views against XSS */ |
66 | private $token; |
67 | |
68 | /** @var Title Title object for target parameter */ |
69 | private $targetObj; |
70 | |
71 | /** @var string Deletion type, may be revision, archive, oldimage, filearchive, logging. */ |
72 | private $typeName; |
73 | |
74 | /** @var array Array of checkbox specs (message, name, deletion bits) */ |
75 | private $checks; |
76 | |
77 | /** @var array UI Labels about the current type */ |
78 | private $typeLabels; |
79 | |
80 | /** @var RevDelList RevDelList object, storing the list of items to be deleted/undeleted */ |
81 | private $revDelList; |
82 | |
83 | /** @var bool Whether user is allowed to perform the action */ |
84 | private $mIsAllowed; |
85 | |
86 | /** @var string */ |
87 | private $otherReason; |
88 | |
89 | private PermissionManager $permissionManager; |
90 | private RepoGroup $repoGroup; |
91 | |
92 | /** |
93 | * UI labels for each type. |
94 | */ |
95 | private const UI_LABELS = [ |
96 | 'revision' => [ |
97 | 'check-label' => 'revdelete-hide-text', |
98 | 'success' => 'revdelete-success', |
99 | 'failure' => 'revdelete-failure', |
100 | 'text' => 'revdelete-text-text', |
101 | 'selected' => 'revdelete-selected-text', |
102 | ], |
103 | 'archive' => [ |
104 | 'check-label' => 'revdelete-hide-text', |
105 | 'success' => 'revdelete-success', |
106 | 'failure' => 'revdelete-failure', |
107 | 'text' => 'revdelete-text-text', |
108 | 'selected' => 'revdelete-selected-text', |
109 | ], |
110 | 'oldimage' => [ |
111 | 'check-label' => 'revdelete-hide-image', |
112 | 'success' => 'revdelete-success', |
113 | 'failure' => 'revdelete-failure', |
114 | 'text' => 'revdelete-text-file', |
115 | 'selected' => 'revdelete-selected-file', |
116 | ], |
117 | 'filearchive' => [ |
118 | 'check-label' => 'revdelete-hide-image', |
119 | 'success' => 'revdelete-success', |
120 | 'failure' => 'revdelete-failure', |
121 | 'text' => 'revdelete-text-file', |
122 | 'selected' => 'revdelete-selected-file', |
123 | ], |
124 | 'logging' => [ |
125 | 'check-label' => 'revdelete-hide-name', |
126 | 'success' => 'logdelete-success', |
127 | 'failure' => 'logdelete-failure', |
128 | 'text' => 'logdelete-text', |
129 | 'selected' => 'logdelete-selected', |
130 | ], |
131 | ]; |
132 | |
133 | /** |
134 | * @inheritDoc |
135 | * |
136 | * @param PermissionManager $permissionManager |
137 | * @param RepoGroup $repoGroup |
138 | */ |
139 | public function __construct( PermissionManager $permissionManager, RepoGroup $repoGroup ) { |
140 | parent::__construct( 'Revisiondelete' ); |
141 | |
142 | $this->permissionManager = $permissionManager; |
143 | $this->repoGroup = $repoGroup; |
144 | } |
145 | |
146 | public function doesWrites() { |
147 | return true; |
148 | } |
149 | |
150 | public function execute( $par ) { |
151 | $this->useTransactionalTimeLimit(); |
152 | |
153 | $this->checkPermissions(); |
154 | $this->checkReadOnly(); |
155 | |
156 | $output = $this->getOutput(); |
157 | $user = $this->getUser(); |
158 | |
159 | $this->setHeaders(); |
160 | $this->outputHeader(); |
161 | $request = $this->getRequest(); |
162 | $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' ); |
163 | # Handle our many different possible input types. |
164 | $ids = $request->getVal( 'ids' ); |
165 | if ( $ids !== null ) { |
166 | # Allow CSV, for backwards compatibility, or a single ID for show/hide links |
167 | $this->ids = explode( ',', $ids ); |
168 | } else { |
169 | # Array input |
170 | $this->ids = array_keys( $request->getArray( 'ids', [] ) ); |
171 | } |
172 | // $this->ids = array_map( 'intval', $this->ids ); |
173 | $this->ids = array_unique( array_filter( $this->ids ) ); |
174 | |
175 | $this->typeName = $request->getVal( 'type' ); |
176 | $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); |
177 | |
178 | # For reviewing deleted files... |
179 | $this->archiveName = $request->getVal( 'file' ); |
180 | $this->token = $request->getVal( 'token' ); |
181 | if ( $this->archiveName && $this->targetObj ) { |
182 | $this->tryShowFile( $this->archiveName ); |
183 | |
184 | return; |
185 | } |
186 | |
187 | $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName ); |
188 | |
189 | # No targets? |
190 | if ( !$this->typeName || count( $this->ids ) == 0 ) { |
191 | throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); |
192 | } |
193 | |
194 | $restriction = RevisionDeleter::getRestriction( $this->typeName ); |
195 | |
196 | if ( !$this->getAuthority()->isAllowedAny( $restriction, 'deletedhistory' ) ) { |
197 | throw new PermissionsError( $restriction ); |
198 | } |
199 | |
200 | # Allow the list type to adjust the passed target |
201 | $this->targetObj = RevisionDeleter::suggestTarget( |
202 | $this->typeName, |
203 | $this->targetObj, |
204 | $this->ids |
205 | ); |
206 | |
207 | # We need a target page! |
208 | if ( $this->targetObj === null ) { |
209 | $output->addWikiMsg( 'undelete-header' ); |
210 | |
211 | return; |
212 | } |
213 | |
214 | // Check blocks |
215 | $checkReplica = !$this->submitClicked; |
216 | if ( |
217 | $this->permissionManager->isBlockedFrom( |
218 | $user, |
219 | $this->targetObj, |
220 | $checkReplica |
221 | ) |
222 | ) { |
223 | throw new UserBlockedError( |
224 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null |
225 | $user->getBlock(), |
226 | $user, |
227 | $this->getLanguage(), |
228 | $request->getIP() |
229 | ); |
230 | } |
231 | |
232 | $this->typeLabels = self::UI_LABELS[$this->typeName]; |
233 | $list = $this->getList(); |
234 | $list->reset(); |
235 | $this->mIsAllowed = $this->permissionManager->userHasRight( $user, $restriction ); |
236 | $canViewSuppressedOnly = $this->permissionManager->userHasRight( $user, 'viewsuppressed' ) && |
237 | !$this->permissionManager->userHasRight( $user, 'suppressrevision' ); |
238 | $pageIsSuppressed = $list->areAnySuppressed(); |
239 | $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed ); |
240 | |
241 | $this->otherReason = $request->getVal( 'wpReason', '' ); |
242 | # Give a link to the logs/hist for this page |
243 | $this->showConvenienceLinks(); |
244 | |
245 | # Initialise checkboxes |
246 | $this->checks = [ |
247 | # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name |
248 | [ $this->typeLabels['check-label'], 'wpHidePrimary', |
249 | RevisionDeleter::getRevdelConstant( $this->typeName ) |
250 | ], |
251 | [ 'revdelete-hide-comment', 'wpHideComment', RevisionRecord::DELETED_COMMENT ], |
252 | [ 'revdelete-hide-user', 'wpHideUser', RevisionRecord::DELETED_USER ] |
253 | ]; |
254 | if ( $this->permissionManager->userHasRight( $user, 'suppressrevision' ) ) { |
255 | $this->checks[] = [ 'revdelete-hide-restricted', |
256 | 'wpHideRestricted', RevisionRecord::DELETED_RESTRICTED ]; |
257 | } |
258 | |
259 | # Either submit or create our form |
260 | if ( $this->mIsAllowed && $this->submitClicked ) { |
261 | $this->submit(); |
262 | } else { |
263 | $this->showForm(); |
264 | } |
265 | |
266 | if ( $this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) { |
267 | # Show relevant lines from the deletion log |
268 | $deleteLogPage = new LogPage( 'delete' ); |
269 | $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" ); |
270 | LogEventsList::showLogExtract( |
271 | $output, |
272 | 'delete', |
273 | $this->targetObj, |
274 | '', /* user */ |
275 | [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ] |
276 | ); |
277 | } |
278 | # Show relevant lines from the suppression log |
279 | if ( $this->permissionManager->userHasRight( $user, 'suppressionlog' ) ) { |
280 | $suppressLogPage = new LogPage( 'suppress' ); |
281 | $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" ); |
282 | LogEventsList::showLogExtract( |
283 | $output, |
284 | 'suppress', |
285 | $this->targetObj, |
286 | '', |
287 | [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ] |
288 | ); |
289 | } |
290 | } |
291 | |
292 | /** |
293 | * Show some useful links in the subtitle |
294 | */ |
295 | protected function showConvenienceLinks() { |
296 | $linkRenderer = $this->getLinkRenderer(); |
297 | # Give a link to the logs/hist for this page |
298 | if ( $this->targetObj ) { |
299 | // Also set header tabs to be for the target. |
300 | $this->getSkin()->setRelevantTitle( $this->targetObj ); |
301 | |
302 | $links = []; |
303 | $links[] = $linkRenderer->makeKnownLink( |
304 | SpecialPage::getTitleFor( 'Log' ), |
305 | $this->msg( 'viewpagelogs' )->text(), |
306 | [], |
307 | [ 'page' => $this->targetObj->getPrefixedText() ] |
308 | ); |
309 | if ( !$this->targetObj->isSpecialPage() ) { |
310 | # Give a link to the page history |
311 | $links[] = $linkRenderer->makeKnownLink( |
312 | $this->targetObj, |
313 | $this->msg( 'pagehist' )->text(), |
314 | [], |
315 | [ 'action' => 'history' ] |
316 | ); |
317 | # Link to deleted edits |
318 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'undelete' ) ) { |
319 | $undelete = SpecialPage::getTitleFor( 'Undelete' ); |
320 | $links[] = $linkRenderer->makeKnownLink( |
321 | $undelete, |
322 | $this->msg( 'deletedhist' )->text(), |
323 | [], |
324 | [ 'target' => $this->targetObj->getPrefixedDBkey() ] |
325 | ); |
326 | } |
327 | } |
328 | # Logs themselves don't have histories or archived revisions |
329 | $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * Get the condition used for fetching log snippets |
335 | * @return array |
336 | */ |
337 | protected function getLogQueryCond() { |
338 | $conds = []; |
339 | // Revision delete logs for these item |
340 | $conds['log_type'] = [ 'delete', 'suppress' ]; |
341 | $conds['log_action'] = $this->getList()->getLogAction(); |
342 | $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName ); |
343 | // Convert IDs to strings, since ls_value is a text field. This avoids |
344 | // a fatal error in PostgreSQL: "operator does not exist: text = integer". |
345 | $conds['ls_value'] = array_map( 'strval', $this->ids ); |
346 | |
347 | return $conds; |
348 | } |
349 | |
350 | /** |
351 | * Show a deleted file version requested by the visitor. |
352 | * @todo Mostly copied from Special:Undelete. Refactor. |
353 | * @param string $archiveName |
354 | */ |
355 | protected function tryShowFile( $archiveName ) { |
356 | $repo = $this->repoGroup->getLocalRepo(); |
357 | $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName ); |
358 | $oimage->load(); |
359 | // Check if user is allowed to see this file |
360 | if ( !$oimage->exists() ) { |
361 | $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); |
362 | |
363 | return; |
364 | } |
365 | $user = $this->getUser(); |
366 | if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) { |
367 | if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { |
368 | throw new PermissionsError( 'suppressrevision' ); |
369 | } else { |
370 | throw new PermissionsError( 'deletedtext' ); |
371 | } |
372 | } |
373 | if ( !$user->matchEditToken( $this->token, $archiveName ) ) { |
374 | $lang = $this->getLanguage(); |
375 | $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm', |
376 | $this->targetObj->getText(), |
377 | $lang->userDate( $oimage->getTimestamp(), $user ), |
378 | $lang->userTime( $oimage->getTimestamp(), $user ) ); |
379 | $this->getOutput()->addHTML( |
380 | Xml::openElement( 'form', [ |
381 | 'method' => 'POST', |
382 | 'action' => $this->getPageTitle()->getLocalURL( [ |
383 | 'target' => $this->targetObj->getPrefixedDBkey(), |
384 | 'file' => $archiveName, |
385 | 'token' => $user->getEditToken( $archiveName ), |
386 | ] ) |
387 | ] |
388 | ) . |
389 | Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) . |
390 | '</form>' |
391 | ); |
392 | |
393 | return; |
394 | } |
395 | $this->getOutput()->disable(); |
396 | # We mustn't allow the output to be CDN cached, otherwise |
397 | # if an admin previews a deleted image, and it's cached, then |
398 | # a user without appropriate permissions can toddle off and |
399 | # nab the image, and CDN will serve it |
400 | $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
401 | $this->getRequest()->response()->header( |
402 | 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' |
403 | ); |
404 | |
405 | $key = $oimage->getStorageKey(); |
406 | $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; |
407 | $repo->streamFileWithStatus( $path ); |
408 | } |
409 | |
410 | /** |
411 | * Get the list object for this request |
412 | * @return RevDelList |
413 | */ |
414 | protected function getList() { |
415 | if ( $this->revDelList === null ) { |
416 | $this->revDelList = RevisionDeleter::createList( |
417 | $this->typeName, $this->getContext(), $this->targetObj, $this->ids |
418 | ); |
419 | } |
420 | |
421 | return $this->revDelList; |
422 | } |
423 | |
424 | /** |
425 | * Show a list of items that we will operate on, and show a form with checkboxes |
426 | * which will allow the user to choose new visibility settings. |
427 | */ |
428 | protected function showForm() { |
429 | $userAllowed = true; |
430 | |
431 | // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected |
432 | $out = $this->getOutput(); |
433 | $out->wrapWikiMsg( "<strong>$1</strong>", [ $this->typeLabels['selected'], |
434 | $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] ); |
435 | |
436 | $this->addHelpLink( 'Help:RevisionDelete' ); |
437 | $out->addHTML( "<ul>" ); |
438 | |
439 | $numRevisions = 0; |
440 | // Live revisions... |
441 | $list = $this->getList(); |
442 | for ( $list->reset(); $list->current(); $list->next() ) { |
443 | $item = $list->current(); |
444 | |
445 | if ( !$item->canView() ) { |
446 | if ( !$this->submitClicked ) { |
447 | throw new PermissionsError( 'suppressrevision' ); |
448 | } |
449 | $userAllowed = false; |
450 | } |
451 | |
452 | $numRevisions++; |
453 | $out->addHTML( $item->getHTML() ); |
454 | } |
455 | |
456 | if ( !$numRevisions ) { |
457 | throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); |
458 | } |
459 | |
460 | $out->addHTML( "</ul>" ); |
461 | // Explanation text |
462 | $this->addUsageText(); |
463 | |
464 | // Normal sysops can always see what they did, but can't always change it |
465 | if ( !$userAllowed ) { |
466 | return; |
467 | } |
468 | |
469 | // Show form if the user can submit |
470 | if ( $this->mIsAllowed ) { |
471 | $suppressAllowed = $this->permissionManager |
472 | ->userHasRight( $this->getUser(), 'suppressrevision' ); |
473 | $out->addModules( [ 'mediawiki.misc-authed-ooui' ] ); |
474 | $out->addModuleStyles( [ 'mediawiki.special', |
475 | 'mediawiki.interface.helpers.styles' ] ); |
476 | |
477 | $dropdownReason = $this->msg( 'revdelete-reason-dropdown' ) |
478 | ->page( $this->targetObj )->inContentLanguage()->text(); |
479 | // Add additional specific reasons for suppress |
480 | if ( $suppressAllowed ) { |
481 | $dropdownReason .= "\n" . $this->msg( 'revdelete-reason-dropdown-suppress' ) |
482 | ->page( $this->targetObj )->inContentLanguage()->text(); |
483 | } |
484 | |
485 | $fields = $this->buildCheckBoxes(); |
486 | |
487 | $fields[] = [ |
488 | 'type' => 'select', |
489 | 'label' => $this->msg( 'revdelete-log' )->text(), |
490 | 'cssclass' => 'wpReasonDropDown', |
491 | 'id' => 'wpRevDeleteReasonList', |
492 | 'name' => 'wpRevDeleteReasonList', |
493 | 'options' => Html::listDropdownOptions( |
494 | $dropdownReason, |
495 | [ 'other' => $this->msg( 'revdelete-reasonotherlist' )->text() ] |
496 | ), |
497 | 'default' => $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ) |
498 | ]; |
499 | |
500 | $fields[] = [ |
501 | 'type' => 'text', |
502 | 'label' => $this->msg( 'revdelete-otherreason' )->text(), |
503 | 'name' => 'wpReason', |
504 | 'id' => 'wpReason', |
505 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
506 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
507 | // Unicode codepoints. |
508 | // "- 155" is to leave room for the 'wpRevDeleteReasonList' value. |
509 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155, |
510 | ]; |
511 | |
512 | $fields[] = [ |
513 | 'type' => 'hidden', |
514 | 'name' => 'wpEditToken', |
515 | 'default' => $this->getUser()->getEditToken() |
516 | ]; |
517 | |
518 | $fields[] = [ |
519 | 'type' => 'hidden', |
520 | 'name' => 'target', |
521 | 'default' => $this->targetObj->getPrefixedText() |
522 | ]; |
523 | |
524 | $fields[] = [ |
525 | 'type' => 'hidden', |
526 | 'name' => 'type', |
527 | 'default' => $this->typeName |
528 | ]; |
529 | |
530 | $fields[] = [ |
531 | 'type' => 'hidden', |
532 | 'name' => 'ids', |
533 | 'default' => implode( ',', $this->ids ) |
534 | ]; |
535 | |
536 | $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); |
537 | $htmlForm |
538 | ->setSubmitText( $this->msg( 'revdelete-submit', $numRevisions )->text() ) |
539 | ->setSubmitName( 'wpSubmit' ) |
540 | ->setWrapperLegend( $this->msg( 'revdelete-legend' )->text() ) |
541 | ->setAction( $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ) |
542 | ->loadData(); |
543 | // Show link to edit the dropdown reasons |
544 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'editinterface' ) ) { |
545 | $link = ''; |
546 | $linkRenderer = $this->getLinkRenderer(); |
547 | if ( $suppressAllowed ) { |
548 | $link .= $linkRenderer->makeKnownLink( |
549 | $this->msg( 'revdelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(), |
550 | $this->msg( 'revdelete-edit-reasonlist-suppress' )->text(), |
551 | [], |
552 | [ 'action' => 'edit' ] |
553 | ); |
554 | $link .= $this->msg( 'pipe-separator' )->escaped(); |
555 | } |
556 | $link .= $linkRenderer->makeKnownLink( |
557 | $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(), |
558 | $this->msg( 'revdelete-edit-reasonlist' )->text(), |
559 | [], |
560 | [ 'action' => 'edit' ] |
561 | ); |
562 | $htmlForm->setPostHtml( Xml::tags( 'p', [ 'class' => 'mw-revdel-editreasons' ], $link ) ); |
563 | } |
564 | $out->addHTML( $htmlForm->getHTML( false ) ); |
565 | } |
566 | } |
567 | |
568 | /** |
569 | * Show some introductory text |
570 | * @todo FIXME: Wikimedia-specific policy text |
571 | */ |
572 | protected function addUsageText() { |
573 | // Messages: revdelete-text-text, revdelete-text-file, logdelete-text |
574 | $this->getOutput()->wrapWikiMsg( |
575 | "<strong>$1</strong>\n$2", $this->typeLabels['text'], |
576 | 'revdelete-text-others' |
577 | ); |
578 | |
579 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) { |
580 | $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' ); |
581 | } |
582 | |
583 | if ( $this->mIsAllowed ) { |
584 | $this->getOutput()->addWikiMsg( 'revdelete-confirm' ); |
585 | } |
586 | } |
587 | |
588 | /** |
589 | * @return array $fields |
590 | */ |
591 | protected function buildCheckBoxes() { |
592 | $fields = []; |
593 | |
594 | $type = 'radio'; |
595 | |
596 | $list = $this->getList(); |
597 | |
598 | // If there is just one item, use checkboxes |
599 | if ( $list->length() == 1 ) { |
600 | $list->reset(); |
601 | |
602 | $type = 'check'; |
603 | } |
604 | |
605 | foreach ( $this->checks as $item ) { |
606 | // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, |
607 | // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted |
608 | [ $message, $name, $bitField ] = $item; |
609 | |
610 | $field = [ |
611 | 'type' => $type, |
612 | 'label-raw' => $this->msg( $message )->escaped(), |
613 | 'id' => $name, |
614 | 'flatlist' => true, |
615 | 'name' => $name, |
616 | 'default' => $list->length() == 1 ? $list->current()->getBits() & $bitField : null |
617 | ]; |
618 | |
619 | if ( $bitField == RevisionRecord::DELETED_RESTRICTED ) { |
620 | $field['label-raw'] = "<b>" . $field['label-raw'] . "</b>"; |
621 | if ( $type === 'radio' ) { |
622 | $field['options-messages'] = [ |
623 | 'revdelete-radio-same' => -1, |
624 | 'revdelete-radio-unset-suppress' => 0, |
625 | 'revdelete-radio-set-suppress' => 1 |
626 | ]; |
627 | } |
628 | } elseif ( $type === 'radio' ) { |
629 | $field['options-messages'] = [ |
630 | 'revdelete-radio-same' => -1, |
631 | 'revdelete-radio-unset' => 0, |
632 | 'revdelete-radio-set' => 1 |
633 | ]; |
634 | } |
635 | |
636 | $fields[] = $field; |
637 | } |
638 | |
639 | return $fields; |
640 | } |
641 | |
642 | /** |
643 | * UI entry point for form submission. |
644 | * @throws PermissionsError |
645 | * @return bool |
646 | */ |
647 | protected function submit() { |
648 | # Check edit token on submission |
649 | $token = $this->getRequest()->getVal( 'wpEditToken' ); |
650 | if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { |
651 | $this->getOutput()->addWikiMsg( 'sessionfailure' ); |
652 | |
653 | return false; |
654 | } |
655 | $bitParams = $this->extractBitParams(); |
656 | // from dropdown |
657 | $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); |
658 | $comment = $listReason; |
659 | if ( $comment === 'other' ) { |
660 | $comment = $this->otherReason; |
661 | } elseif ( $this->otherReason !== '' ) { |
662 | // Entry from drop down menu + additional comment |
663 | $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() |
664 | . $this->otherReason; |
665 | } |
666 | # Can the user set this field? |
667 | if ( $bitParams[RevisionRecord::DELETED_RESTRICTED] == 1 |
668 | && !$this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) |
669 | ) { |
670 | throw new PermissionsError( 'suppressrevision' ); |
671 | } |
672 | # If the save went through, go to success message... |
673 | $status = $this->save( $bitParams, $comment ); |
674 | if ( $status->isGood() ) { |
675 | $this->success(); |
676 | |
677 | return true; |
678 | } else { |
679 | # ...otherwise, bounce back to form... |
680 | $this->failure( $status ); |
681 | } |
682 | |
683 | return false; |
684 | } |
685 | |
686 | /** |
687 | * Report that the submit operation succeeded |
688 | */ |
689 | protected function success() { |
690 | // Messages: revdelete-success, logdelete-success |
691 | $out = $this->getOutput(); |
692 | $out->setPageTitleMsg( $this->msg( 'actioncomplete' ) ); |
693 | $out->addHTML( |
694 | Html::successBox( |
695 | $out->msg( $this->typeLabels['success'] )->parse() |
696 | ) |
697 | ); |
698 | $this->wasSaved = true; |
699 | $this->revDelList->reloadFromPrimary(); |
700 | $this->showForm(); |
701 | } |
702 | |
703 | /** |
704 | * Report that the submit operation failed |
705 | * @param Status $status |
706 | */ |
707 | protected function failure( $status ) { |
708 | // Messages: revdelete-failure, logdelete-failure |
709 | $out = $this->getOutput(); |
710 | $out->setPageTitleMsg( $this->msg( 'actionfailed' ) ); |
711 | $out->addHTML( |
712 | Html::errorBox( |
713 | $out->parseAsContent( |
714 | $status->getWikiText( $this->typeLabels['failure'], false, $this->getLanguage() ) |
715 | ) |
716 | ) |
717 | ); |
718 | $this->showForm(); |
719 | } |
720 | |
721 | /** |
722 | * Put together an array that contains -1, 0, or the *_deleted const for each bit |
723 | * |
724 | * @return array |
725 | */ |
726 | protected function extractBitParams() { |
727 | $bitfield = []; |
728 | foreach ( $this->checks as [ /* message */, $name, $field ] ) { |
729 | $val = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); |
730 | if ( $val < -1 || $val > 1 ) { |
731 | $val = -1; // -1 for existing value |
732 | } |
733 | $bitfield[$field] = $val; |
734 | } |
735 | if ( !isset( $bitfield[RevisionRecord::DELETED_RESTRICTED] ) ) { |
736 | $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0; |
737 | } |
738 | |
739 | return $bitfield; |
740 | } |
741 | |
742 | /** |
743 | * Do the write operations. Simple wrapper for RevDel*List::setVisibility(). |
744 | * @param array $bitPars ExtractBitParams() bitfield array |
745 | * @param string $reason |
746 | * @return Status |
747 | */ |
748 | protected function save( array $bitPars, $reason ) { |
749 | return $this->getList()->setVisibility( |
750 | [ 'value' => $bitPars, 'comment' => $reason ] |
751 | ); |
752 | } |
753 | |
754 | protected function getGroupName() { |
755 | return 'pagetools'; |
756 | } |
757 | } |
758 | |
759 | /** |
760 | * Retain the old class name for backwards compatibility. |
761 | * @deprecated since 1.41 |
762 | */ |
763 | class_alias( SpecialRevisionDelete::class, 'SpecialRevisionDelete' ); |