]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/FileDeleteForm.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / FileDeleteForm.php
1 <?php
2 /**
3  * File deletion user interface.
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  * @author Rob Church <robchur@gmail.com>
22  * @ingroup Media
23  */
24 use MediaWiki\MediaWikiServices;
25
26 /**
27  * File deletion user interface
28  *
29  * @ingroup Media
30  */
31 class FileDeleteForm {
32
33         /**
34          * @var Title
35          */
36         private $title = null;
37
38         /**
39          * @var File
40          */
41         private $file = null;
42
43         /**
44          * @var File
45          */
46         private $oldfile = null;
47         private $oldimage = '';
48
49         /**
50          * @param File $file File object we're deleting
51          */
52         public function __construct( $file ) {
53                 $this->title = $file->getTitle();
54                 $this->file = $file;
55         }
56
57         /**
58          * Fulfil the request; shows the form or deletes the file,
59          * pending authentication, confirmation, etc.
60          */
61         public function execute() {
62                 global $wgOut, $wgRequest, $wgUser, $wgUploadMaintenance;
63
64                 $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $wgUser );
65                 if ( count( $permissionErrors ) ) {
66                         throw new PermissionsError( 'delete', $permissionErrors );
67                 }
68
69                 if ( wfReadOnly() ) {
70                         throw new ReadOnlyError;
71                 }
72
73                 if ( $wgUploadMaintenance ) {
74                         throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' );
75                 }
76
77                 $this->setHeaders();
78
79                 $this->oldimage = $wgRequest->getText( 'oldimage', false );
80                 $token = $wgRequest->getText( 'wpEditToken' );
81                 # Flag to hide all contents of the archived revisions
82                 $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
83
84                 if ( $this->oldimage ) {
85                         $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName(
86                                 $this->title,
87                                 $this->oldimage
88                         );
89                 }
90
91                 if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) {
92                         $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
93                         $wgOut->addReturnTo( $this->title );
94                         return;
95                 }
96
97                 // Perform the deletion if appropriate
98                 if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
99                         $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
100                         $deleteReason = $wgRequest->getText( 'wpReason' );
101
102                         if ( $deleteReasonList == 'other' ) {
103                                 $reason = $deleteReason;
104                         } elseif ( $deleteReason != '' ) {
105                                 // Entry from drop down menu + additional comment
106                                 $reason = $deleteReasonList . wfMessage( 'colon-separator' )
107                                         ->inContentLanguage()->text() . $deleteReason;
108                         } else {
109                                 $reason = $deleteReasonList;
110                         }
111
112                         $status = self::doDelete(
113                                 $this->title,
114                                 $this->file,
115                                 $this->oldimage,
116                                 $reason,
117                                 $suppress,
118                                 $wgUser
119                         );
120
121                         if ( !$status->isGood() ) {
122                                 $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" );
123                                 $wgOut->addWikiText( '<div class="error">' .
124                                         $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
125                                         . '</div>' );
126                         }
127                         if ( $status->isOK() ) {
128                                 $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
129                                 $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) );
130                                 // Return to the main page if we just deleted all versions of the
131                                 // file, otherwise go back to the description page
132                                 $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
133
134                                 WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'wpWatch' ), $this->title, $wgUser );
135                         }
136                         return;
137                 }
138
139                 $this->showForm();
140                 $this->showLogEntries();
141         }
142
143         /**
144          * Really delete the file
145          *
146          * @param Title &$title
147          * @param File &$file
148          * @param string &$oldimage Archive name
149          * @param string $reason Reason of the deletion
150          * @param bool $suppress Whether to mark all deleted versions as restricted
151          * @param User $user User object performing the request
152          * @param array $tags Tags to apply to the deletion action
153          * @throws MWException
154          * @return Status
155          */
156         public static function doDelete( &$title, &$file, &$oldimage, $reason,
157                 $suppress, User $user = null, $tags = []
158         ) {
159                 if ( $user === null ) {
160                         global $wgUser;
161                         $user = $wgUser;
162                 }
163
164                 if ( $oldimage ) {
165                         $page = null;
166                         $status = $file->deleteOld( $oldimage, $reason, $suppress, $user );
167                         if ( $status->ok ) {
168                                 // Need to do a log item
169                                 $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
170                                 if ( trim( $reason ) != '' ) {
171                                         $logComment .= wfMessage( 'colon-separator' )
172                                                 ->inContentLanguage()->text() . $reason;
173                                 }
174
175                                 $logtype = $suppress ? 'suppress' : 'delete';
176
177                                 $logEntry = new ManualLogEntry( $logtype, 'delete' );
178                                 $logEntry->setPerformer( $user );
179                                 $logEntry->setTarget( $title );
180                                 $logEntry->setComment( $logComment );
181                                 $logEntry->setTags( $tags );
182                                 $logid = $logEntry->insert();
183                                 $logEntry->publish( $logid );
184
185                                 $status->value = $logid;
186                         }
187                 } else {
188                         $status = Status::newFatal( 'cannotdelete',
189                                 wfEscapeWikiText( $title->getPrefixedText() )
190                         );
191                         $page = WikiPage::factory( $title );
192                         $dbw = wfGetDB( DB_MASTER );
193                         $dbw->startAtomic( __METHOD__ );
194                         // delete the associated article first
195                         $error = '';
196                         $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error,
197                                 $user, $tags );
198                         // doDeleteArticleReal() returns a non-fatal error status if the page
199                         // or revision is missing, so check for isOK() rather than isGood()
200                         if ( $deleteStatus->isOK() ) {
201                                 $status = $file->delete( $reason, $suppress, $user );
202                                 if ( $status->isOK() ) {
203                                         if ( $deleteStatus->value === null ) {
204                                                 // No log ID from doDeleteArticleReal(), probably
205                                                 // because the page/revision didn't exist, so create
206                                                 // one here.
207                                                 $logtype = $suppress ? 'suppress' : 'delete';
208                                                 $logEntry = new ManualLogEntry( $logtype, 'delete' );
209                                                 $logEntry->setPerformer( $user );
210                                                 $logEntry->setTarget( clone $title );
211                                                 $logEntry->setComment( $reason );
212                                                 $logEntry->setTags( $tags );
213                                                 $logid = $logEntry->insert();
214                                                 $dbw->onTransactionPreCommitOrIdle(
215                                                         function () use ( $dbw, $logEntry, $logid ) {
216                                                                 $logEntry->publish( $logid );
217                                                         },
218                                                         __METHOD__
219                                                 );
220                                                 $status->value = $logid;
221                                         } else {
222                                                 $status->value = $deleteStatus->value; // log id
223                                         }
224                                         $dbw->endAtomic( __METHOD__ );
225                                 } else {
226                                         // Page deleted but file still there? rollback page delete
227                                         $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
228                                         $lbFactory->rollbackMasterChanges( __METHOD__ );
229                                 }
230                         } else {
231                                 // Done; nothing changed
232                                 $dbw->endAtomic( __METHOD__ );
233                         }
234                 }
235
236                 if ( $status->isOK() ) {
237                         Hooks::run( 'FileDeleteComplete', [ &$file, &$oldimage, &$page, &$user, &$reason ] );
238                 }
239
240                 return $status;
241         }
242
243         /**
244          * Show the confirmation form
245          */
246         private function showForm() {
247                 global $wgOut, $wgUser, $wgRequest;
248
249                 if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
250                         $suppress = "<tr id=\"wpDeleteSuppressRow\">
251                                         <td></td>
252                                         <td class='mw-input'><strong>" .
253                                                 Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(),
254                                                         'wpSuppress', 'wpSuppress', false, [ 'tabindex' => '3' ] ) .
255                                         "</strong></td>
256                                 </tr>";
257                 } else {
258                         $suppress = '';
259                 }
260
261                 $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title );
262                 $form = Xml::openElement( 'form', [ 'method' => 'post', 'action' => $this->getAction(),
263                         'id' => 'mw-img-deleteconfirm' ] ) .
264                         Xml::openElement( 'fieldset' ) .
265                         Xml::element( 'legend', null, wfMessage( 'filedelete-legend' )->text() ) .
266                         Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) ) .
267                         $this->prepareMessage( 'filedelete-intro' ) .
268                         Xml::openElement( 'table', [ 'id' => 'mw-img-deleteconfirm-table' ] ) .
269                         "<tr>
270                                 <td class='mw-label'>" .
271                                         Xml::label( wfMessage( 'filedelete-comment' )->text(), 'wpDeleteReasonList' ) .
272                                 "</td>
273                                 <td class='mw-input'>" .
274                                         Xml::listDropDown(
275                                                 'wpDeleteReasonList',
276                                                 wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->text(),
277                                                 wfMessage( 'filedelete-reason-otherlist' )->inContentLanguage()->text(),
278                                                 '',
279                                                 'wpReasonDropDown',
280                                                 1
281                                         ) .
282                                 "</td>
283                         </tr>
284                         <tr>
285                                 <td class='mw-label'>" .
286                                         Xml::label( wfMessage( 'filedelete-otherreason' )->text(), 'wpReason' ) .
287                                 "</td>
288                                 <td class='mw-input'>" .
289                                         Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ),
290                                                 [ 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ] ) .
291                                 "</td>
292                         </tr>
293                         {$suppress}";
294                 if ( $wgUser->isLoggedIn() ) {
295                         $form .= "
296                         <tr>
297                                 <td></td>
298                                 <td class='mw-input'>" .
299                                         Xml::checkLabel( wfMessage( 'watchthis' )->text(),
300                                                 'wpWatch', 'wpWatch', $checkWatch, [ 'tabindex' => '3' ] ) .
301                                 "</td>
302                         </tr>";
303                 }
304                 $form .= "
305                         <tr>
306                                 <td></td>
307                                 <td class='mw-submit'>" .
308                                         Xml::submitButton(
309                                                 wfMessage( 'filedelete-submit' )->text(),
310                                                 [
311                                                         'name' => 'mw-filedelete-submit',
312                                                         'id' => 'mw-filedelete-submit',
313                                                         'tabindex' => '4'
314                                                 ]
315                                         ) .
316                                 "</td>
317                         </tr>" .
318                         Xml::closeElement( 'table' ) .
319                         Xml::closeElement( 'fieldset' ) .
320                         Xml::closeElement( 'form' );
321
322                         if ( $wgUser->isAllowed( 'editinterface' ) ) {
323                                 $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle();
324                                 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
325                                 $link = $linkRenderer->makeKnownLink(
326                                         $title,
327                                         wfMessage( 'filedelete-edit-reasonlist' )->text(),
328                                         [],
329                                         [ 'action' => 'edit' ]
330                                 );
331                                 $form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>';
332                         }
333
334                 $wgOut->addHTML( $form );
335         }
336
337         /**
338          * Show deletion log fragments pertaining to the current file
339          */
340         private function showLogEntries() {
341                 global $wgOut;
342                 $deleteLogPage = new LogPage( 'delete' );
343                 $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" );
344                 LogEventsList::showLogExtract( $wgOut, 'delete', $this->title );
345         }
346
347         /**
348          * Prepare a message referring to the file being deleted,
349          * showing an appropriate message depending upon whether
350          * it's a current file or an old version
351          *
352          * @param string $message Message base
353          * @return string
354          */
355         private function prepareMessage( $message ) {
356                 global $wgLang;
357                 if ( $this->oldimage ) {
358                         # Message keys used:
359                         # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
360                         return wfMessage(
361                                 "{$message}-old",
362                                 wfEscapeWikiText( $this->title->getText() ),
363                                 $wgLang->date( $this->getTimestamp(), true ),
364                                 $wgLang->time( $this->getTimestamp(), true ),
365                                 wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock();
366                 } else {
367                         return wfMessage(
368                                 $message,
369                                 wfEscapeWikiText( $this->title->getText() )
370                         )->parseAsBlock();
371                 }
372         }
373
374         /**
375          * Set headers, titles and other bits
376          */
377         private function setHeaders() {
378                 global $wgOut;
379                 $wgOut->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) );
380                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
381                 $wgOut->addBacklinkSubtitle( $this->title );
382         }
383
384         /**
385          * Is the provided `oldimage` value valid?
386          *
387          * @param string $oldimage
388          * @return bool
389          */
390         public static function isValidOldSpec( $oldimage ) {
391                 return strlen( $oldimage ) >= 16
392                         && strpos( $oldimage, '/' ) === false
393                         && strpos( $oldimage, '\\' ) === false;
394         }
395
396         /**
397          * Could we delete the file specified? If an `oldimage`
398          * value was provided, does it correspond to an
399          * existing, local, old version of this file?
400          *
401          * @param File &$file
402          * @param File &$oldfile
403          * @param File $oldimage
404          * @return bool
405          */
406         public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) {
407                 return $oldimage
408                         ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
409                         : $file && $file->exists() && $file->isLocal();
410         }
411
412         /**
413          * Prepare the form action
414          *
415          * @return string
416          */
417         private function getAction() {
418                 $q = [];
419                 $q['action'] = 'delete';
420
421                 if ( $this->oldimage ) {
422                         $q['oldimage'] = $this->oldimage;
423                 }
424
425                 return $this->title->getLocalURL( $q );
426         }
427
428         /**
429          * Extract the timestamp of the old version
430          *
431          * @return string
432          */
433         private function getTimestamp() {
434                 return $this->oldfile->getTimestamp();
435         }
436 }