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