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