Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 977 |
|
0.00% |
0 / 29 |
CRAP | |
0.00% |
0 / 1 |
SpecialUndelete | |
0.00% |
0 / 976 |
|
0.00% |
0 / 29 |
28730 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
loadRequest | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
462 | |||
isAllowed | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkPermissions | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
execute | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
240 | |||
redirectToRevDel | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
showSearchForm | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
20 | |||
showList | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
20 | |||
showRevision | |
0.00% |
0 / 158 |
|
0.00% |
0 / 1 |
342 | |||
showDiff | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
diffHeader | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
56 | |||
showFileConfirmationForm | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
showFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
addRevisionsToBatch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
addFilesToBatch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
showMoreHistory | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
formatRevisionHistory | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
showHistory | |
0.00% |
0 / 232 |
|
0.00% |
0 / 1 |
650 | |||
formatRevisionRow | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
156 | |||
formatFileRow | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
72 | |||
getPageLink | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
getFileLink | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
20 | |||
getFileUser | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
getFileComment | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
undelete | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
90 | |||
prefixSearchSubpages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Implements Special:Undelete |
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 ArchivedFile; |
27 | use ChangesList; |
28 | use ChangeTags; |
29 | use ErrorPageError; |
30 | use File; |
31 | use IDBAccessObject; |
32 | use LocalRepo; |
33 | use LogEventsList; |
34 | use LogPage; |
35 | use MediaWiki\Cache\LinkBatch; |
36 | use MediaWiki\Cache\LinkBatchFactory; |
37 | use MediaWiki\CommentFormatter\CommentFormatter; |
38 | use MediaWiki\CommentStore\CommentStore; |
39 | use MediaWiki\Content\IContentHandlerFactory; |
40 | use MediaWiki\Context\DerivativeContext; |
41 | use MediaWiki\Html\Html; |
42 | use MediaWiki\Linker\Linker; |
43 | use MediaWiki\MainConfigNames; |
44 | use MediaWiki\Message\Message; |
45 | use MediaWiki\Page\UndeletePage; |
46 | use MediaWiki\Page\UndeletePageFactory; |
47 | use MediaWiki\Page\WikiPageFactory; |
48 | use MediaWiki\Permissions\PermissionManager; |
49 | use MediaWiki\Revision\ArchivedRevisionLookup; |
50 | use MediaWiki\Revision\RevisionArchiveRecord; |
51 | use MediaWiki\Revision\RevisionRecord; |
52 | use MediaWiki\Revision\RevisionRenderer; |
53 | use MediaWiki\Revision\RevisionStore; |
54 | use MediaWiki\Revision\SlotRecord; |
55 | use MediaWiki\SpecialPage\SpecialPage; |
56 | use MediaWiki\Status\Status; |
57 | use MediaWiki\Storage\NameTableAccessException; |
58 | use MediaWiki\Storage\NameTableStore; |
59 | use MediaWiki\Title\Title; |
60 | use MediaWiki\User\Options\UserOptionsLookup; |
61 | use MediaWiki\User\User; |
62 | use MediaWiki\Watchlist\WatchlistManager; |
63 | use OOUI\ActionFieldLayout; |
64 | use OOUI\ButtonInputWidget; |
65 | use OOUI\CheckboxInputWidget; |
66 | use OOUI\DropdownInputWidget; |
67 | use OOUI\FieldLayout; |
68 | use OOUI\FieldsetLayout; |
69 | use OOUI\FormLayout; |
70 | use OOUI\HorizontalLayout; |
71 | use OOUI\HtmlSnippet; |
72 | use OOUI\Layout; |
73 | use OOUI\PanelLayout; |
74 | use OOUI\TextInputWidget; |
75 | use OOUI\Widget; |
76 | use PageArchive; |
77 | use PermissionsError; |
78 | use RepoGroup; |
79 | use SearchEngineFactory; |
80 | use TextContent; |
81 | use UserBlockedError; |
82 | use Wikimedia\Rdbms\IConnectionProvider; |
83 | use Wikimedia\Rdbms\IResultWrapper; |
84 | use Xml; |
85 | |
86 | /** |
87 | * Special page allowing users with the appropriate permissions to view |
88 | * and restore deleted content. |
89 | * |
90 | * @ingroup SpecialPage |
91 | */ |
92 | class SpecialUndelete extends SpecialPage { |
93 | |
94 | /** |
95 | * Limit of revisions (Page history) to display. |
96 | * (If there are more items to display - "Load more" button will appear). |
97 | */ |
98 | private const REVISION_HISTORY_LIMIT = 500; |
99 | |
100 | private $mAction; |
101 | private $mTarget; |
102 | private $mTimestamp; |
103 | private $mRestore; |
104 | private $mRevdel; |
105 | private $mInvert; |
106 | private $mFilename; |
107 | /** @var string[] */ |
108 | private $mTargetTimestamp = []; |
109 | private $mAllowed; |
110 | private $mCanView; |
111 | /** @var string */ |
112 | private $mComment = ''; |
113 | private $mToken; |
114 | /** @var bool|null */ |
115 | private $mPreview; |
116 | /** @var bool|null */ |
117 | private $mDiff; |
118 | /** @var bool|null */ |
119 | private $mDiffOnly; |
120 | /** @var bool|null */ |
121 | private $mUnsuppress; |
122 | /** @var int[] */ |
123 | private $mFileVersions = []; |
124 | /** @var bool|null */ |
125 | private $mUndeleteTalk; |
126 | /** @var string|null Timestamp at which to start a "load more" request (open interval) */ |
127 | private $mHistoryOffset; |
128 | |
129 | /** @var Title|null */ |
130 | private $mTargetObj; |
131 | /** |
132 | * @var string Search prefix |
133 | */ |
134 | private $mSearchPrefix; |
135 | |
136 | private PermissionManager $permissionManager; |
137 | private RevisionStore $revisionStore; |
138 | private RevisionRenderer $revisionRenderer; |
139 | private IContentHandlerFactory $contentHandlerFactory; |
140 | private NameTableStore $changeTagDefStore; |
141 | private LinkBatchFactory $linkBatchFactory; |
142 | private LocalRepo $localRepo; |
143 | private IConnectionProvider $dbProvider; |
144 | private UserOptionsLookup $userOptionsLookup; |
145 | private WikiPageFactory $wikiPageFactory; |
146 | private SearchEngineFactory $searchEngineFactory; |
147 | private UndeletePageFactory $undeletePageFactory; |
148 | private ArchivedRevisionLookup $archivedRevisionLookup; |
149 | private CommentFormatter $commentFormatter; |
150 | private WatchlistManager $watchlistManager; |
151 | |
152 | /** |
153 | * @param PermissionManager $permissionManager |
154 | * @param RevisionStore $revisionStore |
155 | * @param RevisionRenderer $revisionRenderer |
156 | * @param IContentHandlerFactory $contentHandlerFactory |
157 | * @param NameTableStore $changeTagDefStore |
158 | * @param LinkBatchFactory $linkBatchFactory |
159 | * @param RepoGroup $repoGroup |
160 | * @param IConnectionProvider $dbProvider |
161 | * @param UserOptionsLookup $userOptionsLookup |
162 | * @param WikiPageFactory $wikiPageFactory |
163 | * @param SearchEngineFactory $searchEngineFactory |
164 | * @param UndeletePageFactory $undeletePageFactory |
165 | * @param ArchivedRevisionLookup $archivedRevisionLookup |
166 | * @param CommentFormatter $commentFormatter |
167 | * @param WatchlistManager $watchlistManager |
168 | */ |
169 | public function __construct( |
170 | PermissionManager $permissionManager, |
171 | RevisionStore $revisionStore, |
172 | RevisionRenderer $revisionRenderer, |
173 | IContentHandlerFactory $contentHandlerFactory, |
174 | NameTableStore $changeTagDefStore, |
175 | LinkBatchFactory $linkBatchFactory, |
176 | RepoGroup $repoGroup, |
177 | IConnectionProvider $dbProvider, |
178 | UserOptionsLookup $userOptionsLookup, |
179 | WikiPageFactory $wikiPageFactory, |
180 | SearchEngineFactory $searchEngineFactory, |
181 | UndeletePageFactory $undeletePageFactory, |
182 | ArchivedRevisionLookup $archivedRevisionLookup, |
183 | CommentFormatter $commentFormatter, |
184 | WatchlistManager $watchlistManager |
185 | ) { |
186 | parent::__construct( 'Undelete', 'deletedhistory' ); |
187 | $this->permissionManager = $permissionManager; |
188 | $this->revisionStore = $revisionStore; |
189 | $this->revisionRenderer = $revisionRenderer; |
190 | $this->contentHandlerFactory = $contentHandlerFactory; |
191 | $this->changeTagDefStore = $changeTagDefStore; |
192 | $this->linkBatchFactory = $linkBatchFactory; |
193 | $this->localRepo = $repoGroup->getLocalRepo(); |
194 | $this->dbProvider = $dbProvider; |
195 | $this->userOptionsLookup = $userOptionsLookup; |
196 | $this->wikiPageFactory = $wikiPageFactory; |
197 | $this->searchEngineFactory = $searchEngineFactory; |
198 | $this->undeletePageFactory = $undeletePageFactory; |
199 | $this->archivedRevisionLookup = $archivedRevisionLookup; |
200 | $this->commentFormatter = $commentFormatter; |
201 | $this->watchlistManager = $watchlistManager; |
202 | } |
203 | |
204 | public function doesWrites() { |
205 | return true; |
206 | } |
207 | |
208 | private function loadRequest( $par ) { |
209 | $request = $this->getRequest(); |
210 | $user = $this->getUser(); |
211 | |
212 | $this->mAction = $request->getRawVal( 'action' ); |
213 | if ( $par !== null && $par !== '' ) { |
214 | $this->mTarget = $par; |
215 | } else { |
216 | $this->mTarget = $request->getVal( 'target' ); |
217 | } |
218 | |
219 | $this->mTargetObj = null; |
220 | |
221 | if ( $this->mTarget !== null && $this->mTarget !== '' ) { |
222 | $this->mTargetObj = Title::newFromText( $this->mTarget ); |
223 | } |
224 | |
225 | $this->mSearchPrefix = $request->getText( 'prefix' ); |
226 | $time = $request->getVal( 'timestamp' ); |
227 | $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : ''; |
228 | $this->mFilename = $request->getVal( 'file' ); |
229 | |
230 | $posted = $request->wasPosted() && |
231 | $user->matchEditToken( $request->getVal( 'wpEditToken' ) ); |
232 | $this->mRestore = $request->getCheck( 'restore' ) && $posted; |
233 | $this->mRevdel = $request->getCheck( 'revdel' ) && $posted; |
234 | $this->mInvert = $request->getCheck( 'invert' ) && $posted; |
235 | $this->mPreview = $request->getCheck( 'preview' ) && $posted; |
236 | $this->mDiff = $request->getCheck( 'diff' ); |
237 | $this->mDiffOnly = $request->getBool( 'diffonly', |
238 | $this->userOptionsLookup->getOption( $this->getUser(), 'diffonly' ) ); |
239 | $commentList = $request->getText( 'wpCommentList', 'other' ); |
240 | $comment = $request->getText( 'wpComment' ); |
241 | if ( $commentList === 'other' ) { |
242 | $this->mComment = $comment; |
243 | } elseif ( $comment !== '' ) { |
244 | $this->mComment = $commentList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $comment; |
245 | } else { |
246 | $this->mComment = $commentList; |
247 | } |
248 | $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && |
249 | $this->permissionManager->userHasRight( $user, 'suppressrevision' ); |
250 | $this->mToken = $request->getVal( 'token' ); |
251 | $this->mUndeleteTalk = $request->getCheck( 'undeletetalk' ); |
252 | $this->mHistoryOffset = $request->getVal( 'historyoffset' ); |
253 | |
254 | if ( $this->isAllowed( 'undelete' ) ) { |
255 | $this->mAllowed = true; // user can restore |
256 | $this->mCanView = true; // user can view content |
257 | } elseif ( $this->isAllowed( 'deletedtext' ) ) { |
258 | $this->mAllowed = false; // user cannot restore |
259 | $this->mCanView = true; // user can view content |
260 | $this->mRestore = false; |
261 | } else { // user can only view the list of revisions |
262 | $this->mAllowed = false; |
263 | $this->mCanView = false; |
264 | $this->mTimestamp = ''; |
265 | $this->mRestore = false; |
266 | } |
267 | |
268 | if ( $this->mRestore || $this->mInvert ) { |
269 | $timestamps = []; |
270 | $this->mFileVersions = []; |
271 | foreach ( $request->getValues() as $key => $val ) { |
272 | $matches = []; |
273 | if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) { |
274 | $timestamps[] = $matches[1]; |
275 | } |
276 | |
277 | if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) { |
278 | $this->mFileVersions[] = intval( $matches[1] ); |
279 | } |
280 | } |
281 | rsort( $timestamps ); |
282 | $this->mTargetTimestamp = $timestamps; |
283 | } |
284 | } |
285 | |
286 | /** |
287 | * Checks whether a user is allowed the permission for the |
288 | * specific title if one is set. |
289 | * |
290 | * @param string $permission |
291 | * @param User|null $user |
292 | * @return bool |
293 | */ |
294 | protected function isAllowed( $permission, User $user = null ) { |
295 | $user = $user ?: $this->getUser(); |
296 | $block = $user->getBlock(); |
297 | |
298 | if ( $this->mTargetObj !== null ) { |
299 | return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj ); |
300 | } else { |
301 | $hasRight = $this->permissionManager->userHasRight( $user, $permission ); |
302 | $sitewideBlock = $block && $block->isSitewide(); |
303 | return $permission === 'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight; |
304 | } |
305 | } |
306 | |
307 | public function userCanExecute( User $user ) { |
308 | return $this->isAllowed( $this->mRestriction, $user ); |
309 | } |
310 | |
311 | /** |
312 | * @inheritDoc |
313 | */ |
314 | public function checkPermissions() { |
315 | $user = $this->getUser(); |
316 | |
317 | // First check if user has the right to use this page. If not, |
318 | // show a permissions error whether they are blocked or not. |
319 | if ( !parent::userCanExecute( $user ) ) { |
320 | $this->displayRestrictionError(); |
321 | } |
322 | |
323 | // If a user has the right to use this page, but is blocked from |
324 | // the target, show a block error. |
325 | if ( |
326 | $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) { |
327 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null |
328 | throw new UserBlockedError( $user->getBlock() ); |
329 | } |
330 | |
331 | // Finally, do the comprehensive permission check via isAllowed. |
332 | if ( !$this->userCanExecute( $user ) ) { |
333 | $this->displayRestrictionError(); |
334 | } |
335 | } |
336 | |
337 | public function execute( $par ) { |
338 | $this->useTransactionalTimeLimit(); |
339 | |
340 | $user = $this->getUser(); |
341 | |
342 | $this->setHeaders(); |
343 | $this->outputHeader(); |
344 | $this->addHelpLink( 'Help:Deletion_and_undeletion' ); |
345 | |
346 | $this->loadRequest( $par ); |
347 | $this->checkPermissions(); // Needs to be after mTargetObj is set |
348 | |
349 | $out = $this->getOutput(); |
350 | |
351 | if ( $this->mTargetObj === null ) { |
352 | $out->addWikiMsg( 'undelete-header' ); |
353 | |
354 | # Not all users can just browse every deleted page from the list |
355 | if ( $this->permissionManager->userHasRight( $user, 'browsearchive' ) ) { |
356 | $this->showSearchForm(); |
357 | } |
358 | |
359 | return; |
360 | } |
361 | |
362 | $this->addHelpLink( 'Help:Undelete' ); |
363 | if ( $this->mAllowed ) { |
364 | $out->setPageTitleMsg( $this->msg( 'undeletepage' ) ); |
365 | } else { |
366 | $out->setPageTitleMsg( $this->msg( 'viewdeletedpage' ) ); |
367 | } |
368 | |
369 | $this->getSkin()->setRelevantTitle( $this->mTargetObj ); |
370 | |
371 | if ( $this->mTimestamp !== '' ) { |
372 | $this->showRevision( $this->mTimestamp ); |
373 | } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) { |
374 | $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename ); |
375 | // Check if user is allowed to see this file |
376 | if ( !$file->exists() ) { |
377 | $out->addWikiMsg( 'filedelete-nofile', $this->mFilename ); |
378 | } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) { |
379 | if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) { |
380 | throw new PermissionsError( 'suppressrevision' ); |
381 | } else { |
382 | throw new PermissionsError( 'deletedtext' ); |
383 | } |
384 | } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) { |
385 | $this->showFileConfirmationForm( $this->mFilename ); |
386 | } else { |
387 | $this->showFile( $this->mFilename ); |
388 | } |
389 | } elseif ( $this->mAction === 'submit' ) { |
390 | if ( $this->mRestore ) { |
391 | $this->undelete(); |
392 | } elseif ( $this->mRevdel ) { |
393 | $this->redirectToRevDel(); |
394 | } |
395 | } elseif ( $this->mAction === 'render' ) { |
396 | $this->showMoreHistory(); |
397 | } else { |
398 | $this->showHistory(); |
399 | } |
400 | } |
401 | |
402 | /** |
403 | * Convert submitted form data to format expected by RevisionDelete and |
404 | * redirect the request |
405 | */ |
406 | private function redirectToRevDel() { |
407 | $revisions = []; |
408 | |
409 | foreach ( $this->getRequest()->getValues() as $key => $val ) { |
410 | $matches = []; |
411 | if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) { |
412 | $revisionRecord = $this->archivedRevisionLookup |
413 | ->getRevisionRecordByTimestamp( $this->mTargetObj, $matches[1] ); |
414 | if ( $revisionRecord ) { |
415 | // Can return null |
416 | $revisions[ $revisionRecord->getId() ] = 1; |
417 | } |
418 | } |
419 | } |
420 | |
421 | $query = [ |
422 | 'type' => 'revision', |
423 | 'ids' => $revisions, |
424 | 'target' => $this->mTargetObj->getPrefixedText() |
425 | ]; |
426 | $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query ); |
427 | $this->getOutput()->redirect( $url ); |
428 | } |
429 | |
430 | private function showSearchForm() { |
431 | $out = $this->getOutput(); |
432 | $out->setPageTitleMsg( $this->msg( 'undelete-search-title' ) ); |
433 | $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', '1' ); |
434 | |
435 | $out->enableOOUI(); |
436 | |
437 | $fields = []; |
438 | $fields[] = new ActionFieldLayout( |
439 | new TextInputWidget( [ |
440 | 'name' => 'prefix', |
441 | 'inputId' => 'prefix', |
442 | 'infusable' => true, |
443 | 'value' => $this->mSearchPrefix, |
444 | 'autofocus' => true, |
445 | ] ), |
446 | new ButtonInputWidget( [ |
447 | 'label' => $this->msg( 'undelete-search-submit' )->text(), |
448 | 'flags' => [ 'primary', 'progressive' ], |
449 | 'inputId' => 'searchUndelete', |
450 | 'type' => 'submit', |
451 | ] ), |
452 | [ |
453 | 'label' => new HtmlSnippet( |
454 | $this->msg( |
455 | $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix' |
456 | )->parse() |
457 | ), |
458 | 'align' => 'left', |
459 | ] |
460 | ); |
461 | |
462 | $fieldset = new FieldsetLayout( [ |
463 | 'label' => $this->msg( 'undelete-search-box' )->text(), |
464 | 'items' => $fields, |
465 | ] ); |
466 | |
467 | $form = new FormLayout( [ |
468 | 'method' => 'get', |
469 | 'action' => wfScript(), |
470 | ] ); |
471 | |
472 | $form->appendContent( |
473 | $fieldset, |
474 | new HtmlSnippet( |
475 | Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . |
476 | Html::hidden( 'fuzzy', $fuzzySearch ) |
477 | ) |
478 | ); |
479 | |
480 | $out->addHTML( |
481 | new PanelLayout( [ |
482 | 'expanded' => false, |
483 | 'padded' => true, |
484 | 'framed' => true, |
485 | 'content' => $form, |
486 | ] ) |
487 | ); |
488 | |
489 | # List undeletable articles |
490 | if ( $this->mSearchPrefix ) { |
491 | // For now, we enable search engine match only when specifically asked to |
492 | // by using fuzzy=1 parameter. |
493 | if ( $fuzzySearch ) { |
494 | $result = PageArchive::listPagesBySearch( $this->mSearchPrefix ); |
495 | } else { |
496 | $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix ); |
497 | } |
498 | $this->showList( $result ); |
499 | } |
500 | } |
501 | |
502 | /** |
503 | * Generic list of deleted pages |
504 | * |
505 | * @param IResultWrapper $result |
506 | * @return bool |
507 | */ |
508 | private function showList( $result ) { |
509 | $out = $this->getOutput(); |
510 | |
511 | if ( $result->numRows() == 0 ) { |
512 | $out->addWikiMsg( 'undelete-no-results' ); |
513 | |
514 | return false; |
515 | } |
516 | |
517 | $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) ); |
518 | |
519 | $linkRenderer = $this->getLinkRenderer(); |
520 | $undelete = $this->getPageTitle(); |
521 | $out->addHTML( "<ul id='undeleteResultsList'>\n" ); |
522 | foreach ( $result as $row ) { |
523 | $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); |
524 | if ( $title !== null ) { |
525 | $item = $linkRenderer->makeKnownLink( |
526 | $undelete, |
527 | $title->getPrefixedText(), |
528 | [], |
529 | [ 'target' => $title->getPrefixedText() ] |
530 | ); |
531 | } else { |
532 | // The title is no longer valid, show as text |
533 | $item = Html::element( |
534 | 'span', |
535 | [ 'class' => 'mw-invalidtitle' ], |
536 | Linker::getInvalidTitleDescription( |
537 | $this->getContext(), |
538 | $row->ar_namespace, |
539 | $row->ar_title |
540 | ) |
541 | ); |
542 | } |
543 | $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse(); |
544 | $out->addHTML( |
545 | Html::rawElement( |
546 | 'li', |
547 | [ 'class' => 'undeleteResult' ], |
548 | $item . $this->msg( 'word-separator' )->escaped() . |
549 | $this->msg( 'parentheses' )->rawParams( $revs )->escaped() |
550 | ) |
551 | ); |
552 | } |
553 | $result->free(); |
554 | $out->addHTML( "</ul>\n" ); |
555 | |
556 | return true; |
557 | } |
558 | |
559 | private function showRevision( $timestamp ) { |
560 | if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) { |
561 | return; |
562 | } |
563 | $out = $this->getOutput(); |
564 | $out->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
565 | |
566 | // When viewing a specific revision, add a subtitle link back to the overall |
567 | // history, see T284114 |
568 | $listLink = $this->getLinkRenderer()->makeKnownLink( |
569 | $this->getPageTitle(), |
570 | $this->msg( 'undelete-back-to-list' )->text(), |
571 | [], |
572 | [ 'target' => $this->mTargetObj->getPrefixedText() ] |
573 | ); |
574 | // same < arrow as with subpages |
575 | $subtitle = "< $listLink"; |
576 | $out->setSubtitle( $subtitle ); |
577 | |
578 | $archive = new PageArchive( $this->mTargetObj ); |
579 | // FIXME: This hook must be deprecated, passing PageArchive by ref is awful. |
580 | if ( !$this->getHookRunner()->onUndeleteForm__showRevision( |
581 | $archive, $this->mTargetObj ) |
582 | ) { |
583 | return; |
584 | } |
585 | $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp ); |
586 | |
587 | $user = $this->getUser(); |
588 | |
589 | if ( !$revRecord ) { |
590 | $out->addWikiMsg( 'undeleterevision-missing' ); |
591 | return; |
592 | } |
593 | |
594 | if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
595 | // Used in wikilinks, should not contain whitespaces |
596 | $titleText = $this->mTargetObj->getPrefixedDBkey(); |
597 | if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
598 | $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) |
599 | ? [ 'rev-suppressed-text-permission', $titleText ] |
600 | : [ 'rev-deleted-text-permission', $titleText ]; |
601 | $out->addHTML( |
602 | Html::warningBox( |
603 | $this->msg( $msg[0], $msg[1] )->parse(), |
604 | 'plainlinks' |
605 | ) |
606 | ); |
607 | return; |
608 | } |
609 | |
610 | $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) |
611 | ? [ 'rev-suppressed-text-view', $titleText ] |
612 | : [ 'rev-deleted-text-view', $titleText ]; |
613 | $out->addHTML( |
614 | Html::warningBox( |
615 | $this->msg( $msg[0], $msg[1] )->parse(), |
616 | 'plainlinks' |
617 | ) |
618 | ); |
619 | // and we are allowed to see... |
620 | } |
621 | |
622 | if ( $this->mDiff ) { |
623 | $previousRevRecord = $this->archivedRevisionLookup |
624 | ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp ); |
625 | if ( $previousRevRecord ) { |
626 | $this->showDiff( $previousRevRecord, $revRecord ); |
627 | if ( $this->mDiffOnly ) { |
628 | return; |
629 | } |
630 | |
631 | $out->addHTML( '<hr />' ); |
632 | } else { |
633 | $out->addWikiMsg( 'undelete-nodiff' ); |
634 | } |
635 | } |
636 | |
637 | $link = $this->getLinkRenderer()->makeKnownLink( |
638 | $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ), |
639 | $this->mTargetObj->getPrefixedText() |
640 | ); |
641 | |
642 | $lang = $this->getLanguage(); |
643 | |
644 | // date and time are separate parameters to facilitate localisation. |
645 | // $time is kept for backward compat reasons. |
646 | $time = $lang->userTimeAndDate( $timestamp, $user ); |
647 | $d = $lang->userDate( $timestamp, $user ); |
648 | $t = $lang->userTime( $timestamp, $user ); |
649 | $userLink = Linker::revUserTools( $revRecord ); |
650 | |
651 | $content = $revRecord->getContent( |
652 | SlotRecord::MAIN, |
653 | RevisionRecord::FOR_THIS_USER, |
654 | $user |
655 | ); |
656 | |
657 | // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots |
658 | $isText = ( $content instanceof TextContent ); |
659 | |
660 | $undeleteRevisionContent = ''; |
661 | // Revision delete links |
662 | if ( !$this->mDiff ) { |
663 | $revdel = Linker::getRevDeleteLink( |
664 | $user, |
665 | $revRecord, |
666 | $this->mTargetObj |
667 | ); |
668 | if ( $revdel ) { |
669 | $undeleteRevisionContent = $revdel . ' '; |
670 | } |
671 | } |
672 | |
673 | $undeleteRevisionContent .= $out->msg( |
674 | 'undelete-revision', |
675 | Message::rawParam( $link ), |
676 | $time, |
677 | Message::rawParam( $userLink ), |
678 | $d, |
679 | $t |
680 | )->parseAsBlock(); |
681 | |
682 | if ( $this->mPreview || $isText ) { |
683 | $out->addHTML( |
684 | Html::warningBox( |
685 | $undeleteRevisionContent, |
686 | 'mw-undelete-revision' |
687 | ) |
688 | ); |
689 | } else { |
690 | $out->addHTML( |
691 | Html::rawElement( |
692 | 'div', |
693 | [ 'class' => 'mw-undelete-revision', ], |
694 | $undeleteRevisionContent |
695 | ) |
696 | ); |
697 | } |
698 | |
699 | if ( $this->mPreview || !$isText ) { |
700 | // NOTE: non-text content has no source view, so always use rendered preview |
701 | |
702 | $popts = $out->parserOptions(); |
703 | |
704 | $rendered = $this->revisionRenderer->getRenderedRevision( |
705 | $revRecord, |
706 | $popts, |
707 | $user, |
708 | [ 'audience' => RevisionRecord::FOR_THIS_USER, 'causeAction' => 'undelete-preview' ] |
709 | ); |
710 | |
711 | // Fail hard if the audience check fails, since we already checked |
712 | // at the beginning of this method. |
713 | $pout = $rendered->getRevisionParserOutput(); |
714 | |
715 | $out->addParserOutput( $pout, [ |
716 | 'enableSectionEditLinks' => false, |
717 | ] ); |
718 | } |
719 | |
720 | $out->enableOOUI(); |
721 | $buttonFields = []; |
722 | |
723 | if ( $isText ) { |
724 | '@phan-var TextContent $content'; |
725 | // TODO: MCR: make this work for multiple slots |
726 | // source view for textual content |
727 | $sourceView = Xml::element( 'textarea', [ |
728 | 'readonly' => 'readonly', |
729 | 'cols' => 80, |
730 | 'rows' => 25 |
731 | ], $content->getText() . "\n" ); |
732 | |
733 | $buttonFields[] = new ButtonInputWidget( [ |
734 | 'type' => 'submit', |
735 | 'name' => 'preview', |
736 | 'label' => $this->msg( 'showpreview' )->text() |
737 | ] ); |
738 | } else { |
739 | $sourceView = ''; |
740 | } |
741 | |
742 | $buttonFields[] = new ButtonInputWidget( [ |
743 | 'name' => 'diff', |
744 | 'type' => 'submit', |
745 | 'label' => $this->msg( 'showdiff' )->text() |
746 | ] ); |
747 | |
748 | $out->addHTML( |
749 | $sourceView . |
750 | Xml::openElement( 'div', [ |
751 | 'style' => 'clear: both' ] ) . |
752 | Xml::openElement( 'form', [ |
753 | 'method' => 'post', |
754 | 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) . |
755 | Xml::element( 'input', [ |
756 | 'type' => 'hidden', |
757 | 'name' => 'target', |
758 | 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) . |
759 | Xml::element( 'input', [ |
760 | 'type' => 'hidden', |
761 | 'name' => 'timestamp', |
762 | 'value' => $timestamp ] ) . |
763 | Xml::element( 'input', [ |
764 | 'type' => 'hidden', |
765 | 'name' => 'wpEditToken', |
766 | 'value' => $user->getEditToken() ] ) . |
767 | new FieldLayout( |
768 | new Widget( [ |
769 | 'content' => new HorizontalLayout( [ |
770 | 'items' => $buttonFields |
771 | ] ) |
772 | ] ) |
773 | ) . |
774 | Xml::closeElement( 'form' ) . |
775 | Xml::closeElement( 'div' ) |
776 | ); |
777 | } |
778 | |
779 | /** |
780 | * Build a diff display between this and the previous either deleted |
781 | * or non-deleted edit. |
782 | * |
783 | * @param RevisionRecord $previousRevRecord |
784 | * @param RevisionRecord $currentRevRecord |
785 | */ |
786 | private function showDiff( |
787 | RevisionRecord $previousRevRecord, |
788 | RevisionRecord $currentRevRecord |
789 | ) { |
790 | $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() ); |
791 | |
792 | $diffContext = new DerivativeContext( $this->getContext() ); |
793 | $diffContext->setTitle( $currentTitle ); |
794 | $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) ); |
795 | |
796 | $contentModel = $currentRevRecord->getSlot( |
797 | SlotRecord::MAIN, |
798 | RevisionRecord::RAW |
799 | )->getModel(); |
800 | |
801 | $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel ) |
802 | ->createDifferenceEngine( $diffContext ); |
803 | |
804 | $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord ); |
805 | $diffEngine->showDiffStyle(); |
806 | $formattedDiff = $diffEngine->getDiff( |
807 | $this->diffHeader( $previousRevRecord, 'o' ), |
808 | $this->diffHeader( $currentRevRecord, 'n' ) |
809 | ); |
810 | |
811 | $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" ); |
812 | } |
813 | |
814 | /** |
815 | * @param RevisionRecord $revRecord |
816 | * @param string $prefix |
817 | * @return string |
818 | */ |
819 | private function diffHeader( RevisionRecord $revRecord, $prefix ) { |
820 | if ( $revRecord instanceof RevisionArchiveRecord ) { |
821 | // Revision in the archive table, only viewable via this special page |
822 | $targetPage = $this->getPageTitle(); |
823 | $targetQuery = [ |
824 | 'target' => $this->mTargetObj->getPrefixedText(), |
825 | 'timestamp' => wfTimestamp( TS_MW, $revRecord->getTimestamp() ) |
826 | ]; |
827 | } else { |
828 | // Revision in the revision table, viewable by oldid |
829 | $targetPage = $revRecord->getPageAsLinkTarget(); |
830 | $targetQuery = [ 'oldid' => $revRecord->getId() ]; |
831 | } |
832 | |
833 | // Add show/hide deletion links if available |
834 | $user = $this->getUser(); |
835 | $lang = $this->getLanguage(); |
836 | $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj ); |
837 | |
838 | if ( $rdel ) { |
839 | $rdel = " $rdel"; |
840 | } |
841 | |
842 | $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : ''; |
843 | |
844 | $dbr = $this->dbProvider->getReplicaDatabase(); |
845 | $tagIds = $dbr->newSelectQueryBuilder() |
846 | ->select( 'ct_tag_id' ) |
847 | ->from( 'change_tag' ) |
848 | ->where( [ 'ct_rev_id' => $revRecord->getId() ] ) |
849 | ->caller( __METHOD__ )->fetchFieldValues(); |
850 | $tags = []; |
851 | foreach ( $tagIds as $tagId ) { |
852 | try { |
853 | $tags[] = $this->changeTagDefStore->getName( (int)$tagId ); |
854 | } catch ( NameTableAccessException $exception ) { |
855 | continue; |
856 | } |
857 | } |
858 | $tags = implode( ',', $tags ); |
859 | $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() ); |
860 | $asof = $this->getLinkRenderer()->makeLink( |
861 | $targetPage, |
862 | $this->msg( |
863 | 'revisionasof', |
864 | $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ), |
865 | $lang->userDate( $revRecord->getTimestamp(), $user ), |
866 | $lang->userTime( $revRecord->getTimestamp(), $user ) |
867 | )->text(), |
868 | [], |
869 | $targetQuery |
870 | ); |
871 | if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
872 | $asof = Html::rawElement( |
873 | 'span', |
874 | [ 'class' => Linker::getRevisionDeletedClass( $revRecord ) ], |
875 | $asof |
876 | ); |
877 | } |
878 | |
879 | // FIXME This is reimplementing DifferenceEngine#getRevisionHeader |
880 | // and partially #showDiffPage, but worse |
881 | return '<div id="mw-diff-' . $prefix . 'title1"><strong>' . |
882 | $asof . |
883 | '</strong></div>' . |
884 | '<div id="mw-diff-' . $prefix . 'title2">' . |
885 | Linker::revUserTools( $revRecord ) . '<br />' . |
886 | '</div>' . |
887 | '<div id="mw-diff-' . $prefix . 'title3">' . |
888 | $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel . '<br />' . |
889 | '</div>' . |
890 | '<div id="mw-diff-' . $prefix . 'title5">' . |
891 | $tagSummary[0] . '<br />' . |
892 | '</div>'; |
893 | } |
894 | |
895 | /** |
896 | * Show a form confirming whether a tokenless user really wants to see a file |
897 | * @param string $key |
898 | */ |
899 | private function showFileConfirmationForm( $key ) { |
900 | $out = $this->getOutput(); |
901 | $lang = $this->getLanguage(); |
902 | $user = $this->getUser(); |
903 | $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename ); |
904 | $out->addWikiMsg( 'undelete-show-file-confirm', |
905 | $this->mTargetObj->getText(), |
906 | $lang->userDate( $file->getTimestamp(), $user ), |
907 | $lang->userTime( $file->getTimestamp(), $user ) ); |
908 | $out->addHTML( |
909 | Xml::openElement( 'form', [ |
910 | 'method' => 'POST', |
911 | 'action' => $this->getPageTitle()->getLocalURL( [ |
912 | 'target' => $this->mTarget, |
913 | 'file' => $key, |
914 | 'token' => $user->getEditToken( $key ), |
915 | ] ), |
916 | ] |
917 | ) . |
918 | Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) . |
919 | '</form>' |
920 | ); |
921 | } |
922 | |
923 | /** |
924 | * Show a deleted file version requested by the visitor. |
925 | * @param string $key |
926 | */ |
927 | private function showFile( $key ) { |
928 | $this->getOutput()->disable(); |
929 | |
930 | # We mustn't allow the output to be CDN cached, otherwise |
931 | # if an admin previews a deleted image, and it's cached, then |
932 | # a user without appropriate permissions can toddle off and |
933 | # nab the image, and CDN will serve it |
934 | $response = $this->getRequest()->response(); |
935 | $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
936 | $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); |
937 | |
938 | $path = $this->localRepo->getZonePath( 'deleted' ) . '/' . $this->localRepo->getDeletedHashPath( $key ) . $key; |
939 | $this->localRepo->streamFileWithStatus( $path ); |
940 | } |
941 | |
942 | /** |
943 | * @param LinkBatch $batch |
944 | * @param IResultWrapper $revisions |
945 | */ |
946 | private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) { |
947 | foreach ( $revisions as $row ) { |
948 | $batch->add( NS_USER, $row->ar_user_text ); |
949 | $batch->add( NS_USER_TALK, $row->ar_user_text ); |
950 | } |
951 | } |
952 | |
953 | /** |
954 | * @param LinkBatch $batch |
955 | * @param IResultWrapper $files |
956 | */ |
957 | private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) { |
958 | foreach ( $files as $row ) { |
959 | $batch->add( NS_USER, $row->fa_user_text ); |
960 | $batch->add( NS_USER_TALK, $row->fa_user_text ); |
961 | } |
962 | } |
963 | |
964 | /** |
965 | * Handle XHR "show more history" requests (T249977) |
966 | */ |
967 | protected function showMoreHistory() { |
968 | $out = $this->getOutput(); |
969 | $out->setArticleBodyOnly( true ); |
970 | $dbr = $this->dbProvider->getReplicaDatabase(); |
971 | if ( $this->mHistoryOffset ) { |
972 | $extraConds = [ $dbr->expr( 'ar_timestamp', '<', $dbr->timestamp( $this->mHistoryOffset ) ) ]; |
973 | } else { |
974 | $extraConds = []; |
975 | } |
976 | $revisions = $this->archivedRevisionLookup->listRevisions( |
977 | $this->mTargetObj, |
978 | $extraConds, |
979 | self::REVISION_HISTORY_LIMIT + 1 |
980 | ); |
981 | $batch = $this->linkBatchFactory->newLinkBatch(); |
982 | $this->addRevisionsToBatch( $batch, $revisions ); |
983 | $batch->execute(); |
984 | $out->addHTML( $this->formatRevisionHistory( $revisions ) ); |
985 | |
986 | if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) { |
987 | // Indicate to JS that the "show more" button should remain active |
988 | $out->setStatusCode( 206 ); |
989 | } |
990 | } |
991 | |
992 | /** |
993 | * Generate the <ul> element representing a list of deleted revisions |
994 | * |
995 | * @param IResultWrapper $revisions |
996 | * @return string |
997 | */ |
998 | protected function formatRevisionHistory( IResultWrapper $revisions ) { |
999 | $history = Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] ); |
1000 | |
1001 | // Exclude the last data row if there is more data than history limit amount |
1002 | $numRevisions = $revisions->numRows(); |
1003 | $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT ); |
1004 | $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj ); |
1005 | $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() : null; |
1006 | |
1007 | $revisions->rewind(); |
1008 | for ( $i = 0; $i < $displayCount; $i++ ) { |
1009 | $row = $revisions->fetchObject(); |
1010 | // The $remaining parameter controls diff links and so must |
1011 | // include the undisplayed row beyond the display limit. |
1012 | $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i ); |
1013 | } |
1014 | $history .= Html::closeElement( 'ul' ); |
1015 | return $history; |
1016 | } |
1017 | |
1018 | protected function showHistory() { |
1019 | $this->checkReadOnly(); |
1020 | |
1021 | $out = $this->getOutput(); |
1022 | if ( $this->mAllowed ) { |
1023 | $out->addModules( 'mediawiki.misc-authed-ooui' ); |
1024 | $out->addModuleStyles( 'mediawiki.special' ); |
1025 | } |
1026 | $out->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
1027 | $out->wrapWikiMsg( |
1028 | "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n", |
1029 | [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ] |
1030 | ); |
1031 | |
1032 | $archive = new PageArchive( $this->mTargetObj ); |
1033 | // FIXME: This hook must be deprecated, passing PageArchive by ref is awful. |
1034 | $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj ); |
1035 | |
1036 | $out->addHTML( Html::openElement( 'div', [ 'class' => 'mw-undelete-history' ] ) ); |
1037 | if ( $this->mAllowed ) { |
1038 | $out->addWikiMsg( 'undeletehistory' ); |
1039 | $out->addWikiMsg( 'undeleterevdel' ); |
1040 | } else { |
1041 | $out->addWikiMsg( 'undeletehistorynoadmin' ); |
1042 | } |
1043 | $out->addHTML( Html::closeElement( 'div' ) ); |
1044 | |
1045 | # List all stored revisions |
1046 | $revisions = $this->archivedRevisionLookup->listRevisions( |
1047 | $this->mTargetObj, |
1048 | [], |
1049 | self::REVISION_HISTORY_LIMIT + 1 |
1050 | ); |
1051 | $files = $archive->listFiles(); |
1052 | $numRevisions = $revisions->numRows(); |
1053 | $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT; |
1054 | $haveRevisions = $numRevisions > 0; |
1055 | $haveFiles = $files && $files->numRows() > 0; |
1056 | |
1057 | # Batch existence check on user and talk pages |
1058 | if ( $haveRevisions || $haveFiles ) { |
1059 | $batch = $this->linkBatchFactory->newLinkBatch(); |
1060 | $this->addRevisionsToBatch( $batch, $revisions ); |
1061 | if ( $haveFiles ) { |
1062 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable -- $files is non-null |
1063 | $this->addFilesToBatch( $batch, $files ); |
1064 | } |
1065 | $batch->execute(); |
1066 | } |
1067 | |
1068 | if ( $this->mAllowed ) { |
1069 | $out->enableOOUI(); |
1070 | |
1071 | $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ); |
1072 | # Start the form here |
1073 | $form = new FormLayout( [ |
1074 | 'method' => 'post', |
1075 | 'action' => $action, |
1076 | 'id' => 'undelete', |
1077 | ] ); |
1078 | } |
1079 | |
1080 | # Show relevant lines from the deletion log: |
1081 | $deleteLogPage = new LogPage( 'delete' ); |
1082 | $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" ); |
1083 | LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj ); |
1084 | # Show relevant lines from the suppression log: |
1085 | $suppressLogPage = new LogPage( 'suppress' ); |
1086 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressionlog' ) ) { |
1087 | $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" ); |
1088 | LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj ); |
1089 | } |
1090 | |
1091 | if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { |
1092 | $unsuppressAllowed = $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ); |
1093 | $fields = []; |
1094 | $fields[] = new Layout( [ |
1095 | 'content' => new HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() ) |
1096 | ] ); |
1097 | |
1098 | $dropdownComment = $this->msg( 'undelete-comment-dropdown' ) |
1099 | ->page( $this->mTargetObj )->inContentLanguage()->text(); |
1100 | // Add additional specific reasons for unsuppress |
1101 | if ( $unsuppressAllowed ) { |
1102 | $dropdownComment .= "\n" . $this->msg( 'undelete-comment-dropdown-unsuppress' ) |
1103 | ->page( $this->mTargetObj )->inContentLanguage()->text(); |
1104 | } |
1105 | $options = Xml::listDropdownOptions( |
1106 | $dropdownComment, |
1107 | [ 'other' => $this->msg( 'undeletecommentotherlist' )->text() ] |
1108 | ); |
1109 | $options = Xml::listDropdownOptionsOoui( $options ); |
1110 | |
1111 | $fields[] = new FieldLayout( |
1112 | new DropdownInputWidget( [ |
1113 | 'name' => 'wpCommentList', |
1114 | 'inputId' => 'wpCommentList', |
1115 | 'infusable' => true, |
1116 | 'value' => $this->getRequest()->getText( 'wpCommentList', 'other' ), |
1117 | 'options' => $options, |
1118 | ] ), |
1119 | [ |
1120 | 'label' => $this->msg( 'undeletecomment' )->text(), |
1121 | 'align' => 'top', |
1122 | ] |
1123 | ); |
1124 | |
1125 | $fields[] = new FieldLayout( |
1126 | new TextInputWidget( [ |
1127 | 'name' => 'wpComment', |
1128 | 'inputId' => 'wpComment', |
1129 | 'infusable' => true, |
1130 | 'value' => $this->getRequest()->getText( 'wpComment' ), |
1131 | 'autofocus' => true, |
1132 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
1133 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
1134 | // Unicode codepoints. |
1135 | 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
1136 | ] ), |
1137 | [ |
1138 | 'label' => $this->msg( 'undeleteothercomment' )->text(), |
1139 | 'align' => 'top', |
1140 | ] |
1141 | ); |
1142 | |
1143 | if ( $this->getUser()->isRegistered() ) { |
1144 | $checkWatch = $this->watchlistManager->isWatched( $this->getUser(), $this->mTargetObj ) |
1145 | || $this->getRequest()->getText( 'wpWatch' ); |
1146 | $fields[] = new FieldLayout( |
1147 | new CheckboxInputWidget( [ |
1148 | 'name' => 'wpWatch', |
1149 | 'inputId' => 'mw-undelete-watch', |
1150 | 'value' => '1', |
1151 | 'selected' => $checkWatch, |
1152 | ] ), |
1153 | [ |
1154 | 'label' => $this->msg( 'watchthis' )->text(), |
1155 | 'align' => 'inline', |
1156 | ] |
1157 | ); |
1158 | } |
1159 | |
1160 | if ( $unsuppressAllowed ) { |
1161 | $fields[] = new FieldLayout( |
1162 | new CheckboxInputWidget( [ |
1163 | 'name' => 'wpUnsuppress', |
1164 | 'inputId' => 'mw-undelete-unsuppress', |
1165 | 'value' => '1', |
1166 | ] ), |
1167 | [ |
1168 | 'label' => $this->msg( 'revdelete-unsuppress' )->text(), |
1169 | 'align' => 'inline', |
1170 | ] |
1171 | ); |
1172 | } |
1173 | |
1174 | $undelPage = $this->undeletePageFactory->newUndeletePage( |
1175 | $this->wikiPageFactory->newFromTitle( $this->mTargetObj ), |
1176 | $this->getContext()->getAuthority() |
1177 | ); |
1178 | if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) { |
1179 | $fields[] = new FieldLayout( |
1180 | new CheckboxInputWidget( [ |
1181 | 'name' => 'undeletetalk', |
1182 | 'inputId' => 'mw-undelete-undeletetalk', |
1183 | 'selected' => false, |
1184 | ] ), |
1185 | [ |
1186 | 'label' => $this->msg( 'undelete-undeletetalk' )->text(), |
1187 | 'align' => 'inline', |
1188 | ] |
1189 | ); |
1190 | } |
1191 | |
1192 | $fields[] = new FieldLayout( |
1193 | new Widget( [ |
1194 | 'content' => new HorizontalLayout( [ |
1195 | 'items' => [ |
1196 | new ButtonInputWidget( [ |
1197 | 'name' => 'restore', |
1198 | 'inputId' => 'mw-undelete-submit', |
1199 | 'value' => '1', |
1200 | 'label' => $this->msg( 'undeletebtn' )->text(), |
1201 | 'flags' => [ 'primary', 'progressive' ], |
1202 | 'type' => 'submit', |
1203 | ] ), |
1204 | new ButtonInputWidget( [ |
1205 | 'name' => 'invert', |
1206 | 'inputId' => 'mw-undelete-invert', |
1207 | 'value' => '1', |
1208 | 'label' => $this->msg( 'undeleteinvert' )->text() |
1209 | ] ), |
1210 | ] |
1211 | ] ) |
1212 | ] ) |
1213 | ); |
1214 | |
1215 | $fieldset = new FieldsetLayout( [ |
1216 | 'label' => $this->msg( 'undelete-fieldset-title' )->text(), |
1217 | 'id' => 'mw-undelete-table', |
1218 | 'items' => $fields, |
1219 | ] ); |
1220 | |
1221 | $link = ''; |
1222 | if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) { |
1223 | if ( $unsuppressAllowed ) { |
1224 | $link .= $this->getLinkRenderer()->makeKnownLink( |
1225 | $this->msg( 'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->getTitle(), |
1226 | $this->msg( 'undelete-edit-commentlist-unsuppress' )->text(), |
1227 | [], |
1228 | [ 'action' => 'edit' ] |
1229 | ); |
1230 | $link .= $this->msg( 'pipe-separator' )->escaped(); |
1231 | } |
1232 | $link .= $this->getLinkRenderer()->makeKnownLink( |
1233 | $this->msg( 'undelete-comment-dropdown' )->inContentLanguage()->getTitle(), |
1234 | $this->msg( 'undelete-edit-commentlist' )->text(), |
1235 | [], |
1236 | [ 'action' => 'edit' ] |
1237 | ); |
1238 | |
1239 | $link = Html::rawElement( 'p', [ 'class' => 'mw-undelete-editcomments' ], $link ); |
1240 | } |
1241 | |
1242 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here |
1243 | $form->appendContent( |
1244 | new PanelLayout( [ |
1245 | 'expanded' => false, |
1246 | 'padded' => true, |
1247 | 'framed' => true, |
1248 | 'content' => $fieldset, |
1249 | ] ), |
1250 | new HtmlSnippet( |
1251 | $link . |
1252 | Html::hidden( 'target', $this->mTarget ) . |
1253 | Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) |
1254 | ) |
1255 | ); |
1256 | } |
1257 | |
1258 | $history = ''; |
1259 | $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n"; |
1260 | |
1261 | if ( $haveRevisions ) { |
1262 | # Show the page's stored (deleted) history |
1263 | |
1264 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'deleterevision' ) ) { |
1265 | $history .= Html::element( |
1266 | 'button', |
1267 | [ |
1268 | 'name' => 'revdel', |
1269 | 'type' => 'submit', |
1270 | 'class' => 'deleterevision-log-submit mw-log-deleterevision-button' |
1271 | ], |
1272 | $this->msg( 'showhideselectedversions' )->text() |
1273 | ) . "\n"; |
1274 | } |
1275 | |
1276 | $history .= $this->formatRevisionHistory( $revisions ); |
1277 | |
1278 | if ( $showLoadMore ) { |
1279 | $history .= |
1280 | Html::openElement( 'div' ) . |
1281 | Html::element( |
1282 | 'span', |
1283 | [ 'id' => 'mw-load-more-revisions' ], |
1284 | $this->msg( 'undelete-load-more-revisions' )->text() |
1285 | ) . |
1286 | Html::closeElement( 'div' ) . |
1287 | "\n"; |
1288 | } |
1289 | } else { |
1290 | $out->addWikiMsg( 'nohistory' ); |
1291 | } |
1292 | |
1293 | if ( $haveFiles ) { |
1294 | $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n"; |
1295 | $history .= Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] ); |
1296 | foreach ( $files as $row ) { |
1297 | $history .= $this->formatFileRow( $row ); |
1298 | } |
1299 | $files->free(); |
1300 | $history .= Html::closeElement( 'ul' ); |
1301 | } |
1302 | |
1303 | if ( $this->mAllowed ) { |
1304 | # Slip in the hidden controls here |
1305 | $misc = Html::hidden( 'target', $this->mTarget ); |
1306 | $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); |
1307 | $history .= $misc; |
1308 | |
1309 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here |
1310 | $form->appendContent( new HtmlSnippet( $history ) ); |
1311 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here |
1312 | $out->addHTML( (string)$form ); |
1313 | } else { |
1314 | $out->addHTML( $history ); |
1315 | } |
1316 | |
1317 | return true; |
1318 | } |
1319 | |
1320 | protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { |
1321 | $revRecord = $this->revisionStore->newRevisionFromArchiveRow( |
1322 | $row, |
1323 | IDBAccessObject::READ_NORMAL, |
1324 | $this->mTargetObj |
1325 | ); |
1326 | |
1327 | $revTextSize = ''; |
1328 | $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); |
1329 | // Build checkboxen... |
1330 | if ( $this->mAllowed ) { |
1331 | if ( $this->mInvert ) { |
1332 | if ( in_array( $ts, $this->mTargetTimestamp ) ) { |
1333 | $checkBox = Xml::check( "ts$ts" ); |
1334 | } else { |
1335 | $checkBox = Xml::check( "ts$ts", true ); |
1336 | } |
1337 | } else { |
1338 | $checkBox = Xml::check( "ts$ts" ); |
1339 | } |
1340 | } else { |
1341 | $checkBox = ''; |
1342 | } |
1343 | |
1344 | // Build page & diff links... |
1345 | $user = $this->getUser(); |
1346 | if ( $this->mCanView ) { |
1347 | $titleObj = $this->getPageTitle(); |
1348 | # Last link |
1349 | if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
1350 | $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); |
1351 | $last = $this->msg( 'diff' )->escaped(); |
1352 | } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) { |
1353 | $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts ); |
1354 | $last = $this->getLinkRenderer()->makeKnownLink( |
1355 | $titleObj, |
1356 | $this->msg( 'diff' )->text(), |
1357 | [], |
1358 | [ |
1359 | 'target' => $this->mTargetObj->getPrefixedText(), |
1360 | 'timestamp' => $ts, |
1361 | 'diff' => 'prev' |
1362 | ] |
1363 | ); |
1364 | } else { |
1365 | $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts ); |
1366 | $last = $this->msg( 'diff' )->escaped(); |
1367 | } |
1368 | } else { |
1369 | $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); |
1370 | $last = $this->msg( 'diff' )->escaped(); |
1371 | } |
1372 | |
1373 | // User links |
1374 | $userLink = Linker::revUserTools( $revRecord ); |
1375 | |
1376 | // Minor edit |
1377 | $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : ''; |
1378 | |
1379 | // Revision text size |
1380 | $size = $row->ar_len; |
1381 | if ( $size !== null ) { |
1382 | $revTextSize = Linker::formatRevisionSize( $size ); |
1383 | } |
1384 | |
1385 | // Edit summary |
1386 | $comment = $this->commentFormatter->formatRevision( $revRecord, $user ); |
1387 | |
1388 | // Tags |
1389 | $attribs = []; |
1390 | [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow( |
1391 | $row->ts_tags, |
1392 | 'deletedhistory', |
1393 | $this->getContext() |
1394 | ); |
1395 | if ( $classes ) { |
1396 | $attribs['class'] = implode( ' ', $classes ); |
1397 | } |
1398 | |
1399 | $revisionRow = $this->msg( 'undelete-revision-row2' ) |
1400 | ->rawParams( |
1401 | $checkBox, |
1402 | $last, |
1403 | $pageLink, |
1404 | $userLink, |
1405 | $minor, |
1406 | $revTextSize, |
1407 | $comment, |
1408 | $tagSummary |
1409 | ) |
1410 | ->escaped(); |
1411 | |
1412 | return Xml::tags( 'li', $attribs, $revisionRow ) . "\n"; |
1413 | } |
1414 | |
1415 | private function formatFileRow( $row ) { |
1416 | $file = ArchivedFile::newFromRow( $row ); |
1417 | $ts = wfTimestamp( TS_MW, $row->fa_timestamp ); |
1418 | $user = $this->getUser(); |
1419 | |
1420 | $checkBox = ''; |
1421 | if ( $this->mCanView && $row->fa_storage_key ) { |
1422 | if ( $this->mAllowed ) { |
1423 | $checkBox = Xml::check( 'fileid' . $row->fa_id ); |
1424 | } |
1425 | $key = urlencode( $row->fa_storage_key ); |
1426 | $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key ); |
1427 | } else { |
1428 | $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); |
1429 | } |
1430 | $userLink = $this->getFileUser( $file ); |
1431 | $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text(); |
1432 | $bytes = $this->msg( 'parentheses' ) |
1433 | ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() ) |
1434 | ->plain(); |
1435 | $data = htmlspecialchars( $data . ' ' . $bytes ); |
1436 | $comment = $this->getFileComment( $file ); |
1437 | |
1438 | // Add show/hide deletion links if available |
1439 | $canHide = $this->isAllowed( 'deleterevision' ); |
1440 | if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) { |
1441 | if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { |
1442 | // Revision was hidden from sysops |
1443 | $revdlink = Linker::revDeleteLinkDisabled( $canHide ); |
1444 | } else { |
1445 | $query = [ |
1446 | 'type' => 'filearchive', |
1447 | 'target' => $this->mTargetObj->getPrefixedDBkey(), |
1448 | 'ids' => $row->fa_id |
1449 | ]; |
1450 | $revdlink = Linker::revDeleteLink( $query, |
1451 | $file->isDeleted( File::DELETED_RESTRICTED ), $canHide ); |
1452 | } |
1453 | } else { |
1454 | $revdlink = ''; |
1455 | } |
1456 | |
1457 | return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n"; |
1458 | } |
1459 | |
1460 | /** |
1461 | * Fetch revision text link if it's available to all users |
1462 | * |
1463 | * @param RevisionRecord $revRecord |
1464 | * @param Title $titleObj |
1465 | * @param string $ts Timestamp |
1466 | * @return string |
1467 | */ |
1468 | private function getPageLink( RevisionRecord $revRecord, $titleObj, $ts ) { |
1469 | $user = $this->getUser(); |
1470 | $time = $this->getLanguage()->userTimeAndDate( $ts, $user ); |
1471 | |
1472 | if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
1473 | // TODO The condition cannot be true when the function is called |
1474 | return Html::element( |
1475 | 'span', |
1476 | [ 'class' => 'history-deleted' ], |
1477 | $time |
1478 | ); |
1479 | } |
1480 | |
1481 | $link = $this->getLinkRenderer()->makeKnownLink( |
1482 | $titleObj, |
1483 | $time, |
1484 | [], |
1485 | [ |
1486 | 'target' => $this->mTargetObj->getPrefixedText(), |
1487 | 'timestamp' => $ts |
1488 | ] |
1489 | ); |
1490 | |
1491 | if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
1492 | $class = Linker::getRevisionDeletedClass( $revRecord ); |
1493 | $link = '<span class="' . $class . '">' . $link . '</span>'; |
1494 | } |
1495 | |
1496 | return $link; |
1497 | } |
1498 | |
1499 | /** |
1500 | * Fetch image view link if it's available to all users |
1501 | * |
1502 | * @param File|ArchivedFile $file |
1503 | * @param Title $titleObj |
1504 | * @param string $ts A timestamp |
1505 | * @param string $key A storage key |
1506 | * |
1507 | * @return string HTML fragment |
1508 | */ |
1509 | private function getFileLink( $file, $titleObj, $ts, $key ) { |
1510 | $user = $this->getUser(); |
1511 | $time = $this->getLanguage()->userTimeAndDate( $ts, $user ); |
1512 | |
1513 | if ( !$file->userCan( File::DELETED_FILE, $user ) ) { |
1514 | return Html::element( |
1515 | 'span', |
1516 | [ 'class' => 'history-deleted' ], |
1517 | $time |
1518 | ); |
1519 | } |
1520 | |
1521 | if ( $file->exists() ) { |
1522 | $link = $this->getLinkRenderer()->makeKnownLink( |
1523 | $titleObj, |
1524 | $time, |
1525 | [], |
1526 | [ |
1527 | 'target' => $this->mTargetObj->getPrefixedText(), |
1528 | 'file' => $key, |
1529 | 'token' => $user->getEditToken( $key ) |
1530 | ] |
1531 | ); |
1532 | } else { |
1533 | $link = htmlspecialchars( $time ); |
1534 | } |
1535 | |
1536 | if ( $file->isDeleted( File::DELETED_FILE ) ) { |
1537 | $link = '<span class="history-deleted">' . $link . '</span>'; |
1538 | } |
1539 | |
1540 | return $link; |
1541 | } |
1542 | |
1543 | /** |
1544 | * Fetch file's user id if it's available to this user |
1545 | * |
1546 | * @param File|ArchivedFile $file |
1547 | * @return string HTML fragment |
1548 | */ |
1549 | private function getFileUser( $file ) { |
1550 | $uploader = $file->getUploader( File::FOR_THIS_USER, $this->getAuthority() ); |
1551 | if ( !$uploader ) { |
1552 | return Html::rawElement( |
1553 | 'span', |
1554 | [ 'class' => 'history-deleted' ], |
1555 | $this->msg( 'rev-deleted-user' )->escaped() |
1556 | ); |
1557 | } |
1558 | |
1559 | $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) . |
1560 | Linker::userToolLinks( $uploader->getId(), $uploader->getName() ); |
1561 | |
1562 | if ( $file->isDeleted( File::DELETED_USER ) ) { |
1563 | $link = Html::rawElement( |
1564 | 'span', |
1565 | [ 'class' => 'history-deleted' ], |
1566 | $link |
1567 | ); |
1568 | } |
1569 | |
1570 | return $link; |
1571 | } |
1572 | |
1573 | /** |
1574 | * Fetch file upload comment if it's available to this user |
1575 | * |
1576 | * @param File|ArchivedFile $file |
1577 | * @return string HTML fragment |
1578 | */ |
1579 | private function getFileComment( $file ) { |
1580 | if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) { |
1581 | return Html::rawElement( |
1582 | 'span', |
1583 | [ 'class' => 'history-deleted' ], |
1584 | Html::rawElement( |
1585 | 'span', |
1586 | [ 'class' => 'comment' ], |
1587 | $this->msg( 'rev-deleted-comment' )->escaped() |
1588 | ) |
1589 | ); |
1590 | } |
1591 | |
1592 | $comment = $file->getDescription( File::FOR_THIS_USER, $this->getAuthority() ); |
1593 | $link = $this->commentFormatter->formatBlock( $comment ); |
1594 | |
1595 | if ( $file->isDeleted( File::DELETED_COMMENT ) ) { |
1596 | $link = Html::rawElement( |
1597 | 'span', |
1598 | [ 'class' => 'history-deleted' ], |
1599 | $link |
1600 | ); |
1601 | } |
1602 | |
1603 | return $link; |
1604 | } |
1605 | |
1606 | private function undelete() { |
1607 | if ( $this->getConfig()->get( MainConfigNames::UploadMaintenance ) |
1608 | && $this->mTargetObj->getNamespace() === NS_FILE |
1609 | ) { |
1610 | throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' ); |
1611 | } |
1612 | |
1613 | $this->checkReadOnly(); |
1614 | |
1615 | $out = $this->getOutput(); |
1616 | $undeletePage = $this->undeletePageFactory->newUndeletePage( |
1617 | $this->wikiPageFactory->newFromTitle( $this->mTargetObj ), |
1618 | $this->getAuthority() |
1619 | ); |
1620 | if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) { |
1621 | $undeletePage->setUndeleteAssociatedTalk( true ); |
1622 | } |
1623 | $status = $undeletePage |
1624 | ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp ) |
1625 | ->setUndeleteOnlyFileVersions( $this->mFileVersions ) |
1626 | ->setUnsuppress( $this->mUnsuppress ) |
1627 | // TODO This is currently duplicating some permission checks, but we do need it (T305680) |
1628 | ->undeleteIfAllowed( $this->mComment ); |
1629 | |
1630 | if ( !$status->isGood() ) { |
1631 | $out->setPageTitleMsg( $this->msg( 'undelete-error' ) ); |
1632 | $out->wrapWikiTextAsInterface( |
1633 | 'error', |
1634 | Status::wrap( $status )->getWikiText( |
1635 | 'cannotundelete', |
1636 | 'cannotundelete', |
1637 | $this->getLanguage() |
1638 | ) |
1639 | ); |
1640 | return; |
1641 | } |
1642 | |
1643 | $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED]; |
1644 | $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED]; |
1645 | |
1646 | if ( $restoredRevs === 0 && $restoredFiles === 0 ) { |
1647 | // TODO Should use a different message here |
1648 | $out->setPageTitleMsg( $this->msg( 'undelete-error' ) ); |
1649 | } else { |
1650 | if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) { |
1651 | $this->getHookRunner()->onFileUndeleteComplete( |
1652 | $this->mTargetObj, $this->mFileVersions, $this->getUser(), $this->mComment ); |
1653 | } |
1654 | |
1655 | $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj ); |
1656 | $out->addWikiMsg( 'undeletedpage', Message::rawParam( $link ) ); |
1657 | |
1658 | $this->watchlistManager->setWatch( |
1659 | $this->getRequest()->getCheck( 'wpWatch' ), |
1660 | $this->getAuthority(), |
1661 | $this->mTargetObj |
1662 | ); |
1663 | } |
1664 | } |
1665 | |
1666 | /** |
1667 | * Return an array of subpages beginning with $search that this special page will accept. |
1668 | * |
1669 | * @param string $search Prefix to search for |
1670 | * @param int $limit Maximum number of results to return (usually 10) |
1671 | * @param int $offset Number of results to skip (usually 0) |
1672 | * @return string[] Matching subpages |
1673 | */ |
1674 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
1675 | return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); |
1676 | } |
1677 | |
1678 | protected function getGroupName() { |
1679 | return 'pagetools'; |
1680 | } |
1681 | } |
1682 | |
1683 | /** |
1684 | * Retain the old class name for backwards compatibility. |
1685 | * @deprecated since 1.41 |
1686 | */ |
1687 | class_alias( SpecialUndelete::class, 'SpecialUndelete' ); |