Restore MW 1.4 behaviour for Special:Upload -- redirect to description page on new...
[lhc/web/wiklou.git] / includes / filerepo / LocalFile.php
1 <?php
2 /**
3 */
4
5 /**
6 * Bump this number when serialized cache records may be incompatible.
7 */
8 define( 'MW_FILE_VERSION', 4 );
9
10 /**
11 * Class to represent a local file in the wiki's own database
12 *
13 * Provides methods to retrieve paths (physical, logical, URL),
14 * to generate image thumbnails or for uploading.
15 *
16 * Note that only the repo object knows what its file class is called. You should
17 * never name a file class explictly outside of the repo class. Instead use the
18 * repo's factory functions to generate file objects, for example:
19 *
20 * RepoGroup::singleton()->getLocalRepo()->newFile($title);
21 *
22 * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
23 * in most cases.
24 *
25 * @addtogroup FileRepo
26 */
27 class LocalFile extends File
28 {
29 /**#@+
30 * @private
31 */
32 var $fileExists, # does the file file exist on disk? (loadFromXxx)
33 $historyLine, # Number of line to return by nextHistoryLine() (constructor)
34 $historyRes, # result of the query for the file's history (nextHistoryLine)
35 $width, # \
36 $height, # |
37 $bits, # --- returned by getimagesize (loadFromXxx)
38 $attr, # /
39 $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
40 $mime, # MIME type, determined by MimeMagic::guessMimeType
41 $major_mime, # Major mime type
42 $minor_mine, # Minor mime type
43 $size, # Size in bytes (loadFromXxx)
44 $metadata, # Metadata
45 $timestamp, # Upload timestamp
46 $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
47 $upgraded; # Whether the row was upgraded on load
48
49 /**#@-*/
50
51 /**
52 * Create a LocalFile from a title
53 * Do not call this except from inside a repo class.
54 */
55 static function newFromTitle( $title, $repo ) {
56 return new self( $title, $repo );
57 }
58
59 /**
60 * Create a LocalFile from a title
61 * Do not call this except from inside a repo class.
62 */
63 static function newFromRow( $row, $repo ) {
64 $title = Title::makeTitle( NS_IMAGE, $row->img_name );
65 $file = new self( $title, $repo );
66 $file->loadFromRow( $row );
67 return $file;
68 }
69
70 /**
71 * Constructor.
72 * Do not call this except from inside a repo class.
73 */
74 function __construct( $title, $repo ) {
75 if( !is_object( $title ) ) {
76 throw new MWException( __CLASS__.' constructor given bogus title.' );
77 }
78 parent::__construct( $title, $repo );
79 $this->metadata = '';
80 $this->historyLine = 0;
81 $this->dataLoaded = false;
82 }
83
84 /**
85 * Get the memcached key
86 */
87 function getCacheKey() {
88 $hashedName = md5($this->getName());
89 return wfMemcKey( 'file', $hashedName );
90 }
91
92 /**
93 * Try to load file metadata from memcached. Returns true on success.
94 */
95 function loadFromCache() {
96 global $wgMemc;
97 wfProfileIn( __METHOD__ );
98 $this->dataLoaded = false;
99 $key = $this->getCacheKey();
100 if ( !$key ) {
101 return false;
102 }
103 $cachedValues = $wgMemc->get( $key );
104
105 // Check if the key existed and belongs to this version of MediaWiki
106 if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
107 wfDebug( "Pulling file metadata from cache key $key\n" );
108 $this->fileExists = $cachedValues['fileExists'];
109 if ( $this->fileExists ) {
110 unset( $cachedValues['version'] );
111 unset( $cachedValues['fileExists'] );
112 foreach ( $cachedValues as $name => $value ) {
113 $this->$name = $value;
114 }
115 }
116 }
117 if ( $this->dataLoaded ) {
118 wfIncrStats( 'image_cache_hit' );
119 } else {
120 wfIncrStats( 'image_cache_miss' );
121 }
122
123 wfProfileOut( __METHOD__ );
124 return $this->dataLoaded;
125 }
126
127 /**
128 * Save the file metadata to memcached
129 */
130 function saveToCache() {
131 global $wgMemc;
132 $this->load();
133 $key = $this->getCacheKey();
134 if ( !$key ) {
135 return;
136 }
137 $fields = $this->getCacheFields( '' );
138 $cache = array( 'version' => MW_FILE_VERSION );
139 $cache['fileExists'] = $this->fileExists;
140 if ( $this->fileExists ) {
141 foreach ( $fields as $field ) {
142 $cache[$field] = $this->$field;
143 }
144 }
145
146 $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
147 }
148
149 /**
150 * Load metadata from the file itself
151 */
152 function loadFromFile() {
153 $this->setProps( self::getPropsFromPath( $this->getPath() ) );
154 }
155
156 function getCacheFields( $prefix = 'img_' ) {
157 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
158 'major_mime', 'minor_mime', 'metadata', 'timestamp' );
159 static $results = array();
160 if ( $prefix == '' ) {
161 return $fields;
162 }
163 if ( !isset( $results[$prefix] ) ) {
164 $prefixedFields = array();
165 foreach ( $fields as $field ) {
166 $prefixedFields[] = $prefix . $field;
167 }
168 $results[$prefix] = $prefixedFields;
169 }
170 return $results[$prefix];
171 }
172
173 /**
174 * Load file metadata from the DB
175 */
176 function loadFromDB() {
177 wfProfileIn( __METHOD__ );
178
179 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
180 $this->dataLoaded = true;
181
182 $dbr = $this->repo->getSlaveDB();
183
184 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
185 array( 'img_name' => $this->getName() ), __METHOD__ );
186 if ( $row ) {
187 $this->loadFromRow( $row );
188 } else {
189 $this->fileExists = false;
190 }
191
192 wfProfileOut( __METHOD__ );
193 }
194
195 /**
196 * Decode a row from the database (either object or array) to an array
197 * with timestamps and MIME types decoded, and the field prefix removed.
198 */
199 function decodeRow( $row, $prefix = 'img_' ) {
200 $array = (array)$row;
201 $prefixLength = strlen( $prefix );
202 // Sanity check prefix once
203 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
204 throw new MWException( __METHOD__. ': incorrect $prefix parameter' );
205 }
206 $decoded = array();
207 foreach ( $array as $name => $value ) {
208 $deprefixedName = substr( $name, $prefixLength );
209 $decoded[substr( $name, $prefixLength )] = $value;
210 }
211 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
212 if ( empty( $decoded['major_mime'] ) ) {
213 $decoded['mime'] = "unknown/unknown";
214 } else {
215 if (!$decoded['minor_mime']) {
216 $decoded['minor_mime'] = "unknown";
217 }
218 $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
219 }
220 return $decoded;
221 }
222
223 /*
224 * Load file metadata from a DB result row
225 */
226 function loadFromRow( $row, $prefix = 'img_' ) {
227 $array = $this->decodeRow( $row, $prefix );
228 foreach ( $array as $name => $value ) {
229 $this->$name = $value;
230 }
231 $this->fileExists = true;
232 // Check for rows from a previous schema, quietly upgrade them
233 $this->maybeUpgradeRow();
234 }
235
236 /**
237 * Load file metadata from cache or DB, unless already loaded
238 */
239 function load() {
240 if ( !$this->dataLoaded ) {
241 if ( !$this->loadFromCache() ) {
242 $this->loadFromDB();
243 $this->saveToCache();
244 }
245 $this->dataLoaded = true;
246 }
247 }
248
249 /**
250 * Upgrade a row if it needs it
251 */
252 function maybeUpgradeRow() {
253 if ( wfReadOnly() ) {
254 return;
255 }
256 if ( is_null($this->media_type) || $this->mime == 'image/svg' ) {
257 $this->upgradeRow();
258 $this->upgraded = true;
259 } else {
260 $handler = $this->getHandler();
261 if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
262 $this->upgradeRow();
263 $this->upgraded = true;
264 }
265 }
266 }
267
268 function getUpgraded() {
269 return $this->upgraded;
270 }
271
272 /**
273 * Fix assorted version-related problems with the image row by reloading it from the file
274 */
275 function upgradeRow() {
276 wfProfileIn( __METHOD__ );
277
278 $this->loadFromFile();
279
280 $dbw = $this->repo->getMasterDB();
281 list( $major, $minor ) = self::splitMime( $this->mime );
282
283 wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
284
285 $dbw->update( 'image',
286 array(
287 'img_width' => $this->width,
288 'img_height' => $this->height,
289 'img_bits' => $this->bits,
290 'img_media_type' => $this->media_type,
291 'img_major_mime' => $major,
292 'img_minor_mime' => $minor,
293 'img_metadata' => $this->metadata,
294 ), array( 'img_name' => $this->getName() ),
295 __METHOD__
296 );
297 $this->saveToCache();
298 wfProfileOut( __METHOD__ );
299 }
300
301 function setProps( $info ) {
302 $this->dataLoaded = true;
303 $fields = $this->getCacheFields( '' );
304 $fields[] = 'fileExists';
305 foreach ( $fields as $field ) {
306 if ( isset( $info[$field] ) ) {
307 $this->$field = $info[$field];
308 }
309 }
310 }
311
312 /** splitMime inherited */
313 /** getName inherited */
314 /** getTitle inherited */
315 /** getURL inherited */
316 /** getViewURL inherited */
317 /** getPath inherited */
318
319 /**
320 * Return the width of the image
321 *
322 * Returns false on error
323 * @public
324 */
325 function getWidth( $page = 1 ) {
326 $this->load();
327 if ( $this->isMultipage() ) {
328 $dim = $this->getHandler()->getPageDimensions( $this, $page );
329 if ( $dim ) {
330 return $dim['width'];
331 } else {
332 return false;
333 }
334 } else {
335 return $this->width;
336 }
337 }
338
339 /**
340 * Return the height of the image
341 *
342 * Returns false on error
343 * @public
344 */
345 function getHeight( $page = 1 ) {
346 $this->load();
347 if ( $this->isMultipage() ) {
348 $dim = $this->getHandler()->getPageDimensions( $this, $page );
349 if ( $dim ) {
350 return $dim['height'];
351 } else {
352 return false;
353 }
354 } else {
355 return $this->height;
356 }
357 }
358
359 /**
360 * Get handler-specific metadata
361 */
362 function getMetadata() {
363 $this->load();
364 return $this->metadata;
365 }
366
367 /**
368 * Return the size of the image file, in bytes
369 * @public
370 */
371 function getSize() {
372 $this->load();
373 return $this->size;
374 }
375
376 /**
377 * Returns the mime type of the file.
378 */
379 function getMimeType() {
380 $this->load();
381 return $this->mime;
382 }
383
384 /**
385 * Return the type of the media in the file.
386 * Use the value returned by this function with the MEDIATYPE_xxx constants.
387 */
388 function getMediaType() {
389 $this->load();
390 return $this->media_type;
391 }
392
393 /** canRender inherited */
394 /** mustRender inherited */
395 /** allowInlineDisplay inherited */
396 /** isSafeFile inherited */
397 /** isTrustedFile inherited */
398
399 /**
400 * Returns true if the file file exists on disk.
401 * @return boolean Whether file file exist on disk.
402 * @public
403 */
404 function exists() {
405 $this->load();
406 return $this->fileExists;
407 }
408
409 /** getTransformScript inherited */
410 /** getUnscaledThumb inherited */
411 /** thumbName inherited */
412 /** createThumb inherited */
413 /** getThumbnail inherited */
414 /** transform inherited */
415
416 /**
417 * Fix thumbnail files from 1.4 or before, with extreme prejudice
418 */
419 function migrateThumbFile( $thumbName ) {
420 $thumbDir = $this->getThumbPath();
421 $thumbPath = "$thumbDir/$thumbName";
422 if ( is_dir( $thumbPath ) ) {
423 // Directory where file should be
424 // This happened occasionally due to broken migration code in 1.5
425 // Rename to broken-*
426 for ( $i = 0; $i < 100 ; $i++ ) {
427 $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName";
428 if ( !file_exists( $broken ) ) {
429 rename( $thumbPath, $broken );
430 break;
431 }
432 }
433 // Doesn't exist anymore
434 clearstatcache();
435 }
436 if ( is_file( $thumbDir ) ) {
437 // File where directory should be
438 unlink( $thumbDir );
439 // Doesn't exist anymore
440 clearstatcache();
441 }
442 }
443
444 /** getHandler inherited */
445 /** iconThumb inherited */
446 /** getLastError inherited */
447
448 /**
449 * Get all thumbnail names previously generated for this file
450 */
451 function getThumbnails() {
452 if ( $this->isHashed() ) {
453 $this->load();
454 $files = array();
455 $dir = $this->getThumbPath();
456
457 if ( is_dir( $dir ) ) {
458 $handle = opendir( $dir );
459
460 if ( $handle ) {
461 while ( false !== ( $file = readdir($handle) ) ) {
462 if ( $file{0} != '.' ) {
463 $files[] = $file;
464 }
465 }
466 closedir( $handle );
467 }
468 }
469 } else {
470 $files = array();
471 }
472
473 return $files;
474 }
475
476 /**
477 * Refresh metadata in memcached, but don't touch thumbnails or squid
478 */
479 function purgeMetadataCache() {
480 $this->loadFromDB();
481 $this->saveToCache();
482 }
483
484 /**
485 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
486 */
487 function purgeCache( $archiveFiles = array() ) {
488 // Refresh metadata cache
489 $this->purgeMetadataCache();
490
491 // Delete thumbnails
492 $this->purgeThumbnails();
493
494 // Purge squid cache for this file
495 wfPurgeSquidServers( array( $this->getURL() ) );
496 }
497
498 /**
499 * Delete cached transformed files
500 */
501 function purgeThumbnails() {
502 global $wgUseSquid;
503 // Delete thumbnails
504 $files = $this->getThumbnails();
505 $dir = $this->getThumbPath();
506 $urls = array();
507 foreach ( $files as $file ) {
508 $m = array();
509 # Check that the base file name is part of the thumb name
510 # This is a basic sanity check to avoid erasing unrelated directories
511 if ( strpos( $file, $this->getName() ) !== false ) {
512 $url = $this->getThumbUrl( $file );
513 $urls[] = $url;
514 @unlink( "$dir/$file" );
515 }
516 }
517
518 // Purge the squid
519 if ( $wgUseSquid ) {
520 wfPurgeSquidServers( $urls );
521 }
522 }
523
524 /** purgeDescription inherited */
525 /** purgeEverything inherited */
526
527 /**
528 * Return the history of this file, line by line.
529 * starts with current version, then old versions.
530 * uses $this->historyLine to check which line to return:
531 * 0 return line for current version
532 * 1 query for old versions, return first one
533 * 2, ... return next old version from above query
534 *
535 * @public
536 */
537 function nextHistoryLine() {
538 $dbr = $this->repo->getSlaveDB();
539
540 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
541 $this->historyRes = $dbr->select( 'image',
542 array(
543 'img_size',
544 'img_description',
545 'img_user','img_user_text',
546 'img_timestamp',
547 'img_width',
548 'img_height',
549 "'' AS oi_archive_name"
550 ),
551 array( 'img_name' => $this->title->getDBkey() ),
552 __METHOD__
553 );
554 if ( 0 == $dbr->numRows( $this->historyRes ) ) {
555 return FALSE;
556 }
557 } else if ( $this->historyLine == 1 ) {
558 $this->historyRes = $dbr->select( 'oldimage',
559 array(
560 'oi_size AS img_size',
561 'oi_description AS img_description',
562 'oi_user AS img_user',
563 'oi_user_text AS img_user_text',
564 'oi_timestamp AS img_timestamp',
565 'oi_width as img_width',
566 'oi_height as img_height',
567 'oi_archive_name'
568 ),
569 array( 'oi_name' => $this->title->getDBkey() ),
570 __METHOD__,
571 array( 'ORDER BY' => 'oi_timestamp DESC' )
572 );
573 }
574 $this->historyLine ++;
575
576 return $dbr->fetchObject( $this->historyRes );
577 }
578
579 /**
580 * Reset the history pointer to the first element of the history
581 * @public
582 */
583 function resetHistory() {
584 $this->historyLine = 0;
585 }
586
587 /** getFullPath inherited */
588 /** getHashPath inherited */
589 /** getRel inherited */
590 /** getUrlRel inherited */
591 /** getArchivePath inherited */
592 /** getThumbPath inherited */
593 /** getArchiveUrl inherited */
594 /** getThumbUrl inherited */
595 /** getArchiveVirtualUrl inherited */
596 /** getThumbVirtualUrl inherited */
597 /** isHashed inherited */
598
599 /**
600 * Upload a file and record it in the DB
601 * @param string $srcPath Source path or virtual URL
602 * @param string $comment Upload description
603 * @param string $pageText Text to use for the new description page, if a new description page is created
604 * @param integer $flags Flags for publish()
605 * @param array $props File properties, if known. This can be used to reduce the
606 * upload time when uploading virtual URLs for which the file info
607 * is already known
608 * @param string $timestamp Timestamp for img_timestamp, or false to use the current time
609 *
610 * @return Returns the archive name on success or an empty string if it was a new upload.
611 * Returns a wikitext-formatted WikiError on failure.
612 */
613 function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) {
614 $archive = $this->publish( $srcPath, $flags );
615 if ( WikiError::isError( $archive ) ){
616 return $archive;
617 }
618 if ( !$this->recordUpload2( $archive, $comment, $pageText, $props, $timestamp ) ) {
619 return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) );
620 }
621 return $archive;
622 }
623
624 /**
625 * Record a file upload in the upload log and the image table
626 * @deprecated use upload()
627 */
628 function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
629 $watch = false, $timestamp = false )
630 {
631 $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source );
632 if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
633 return false;
634 }
635 if ( $watch ) {
636 global $wgUser;
637 $wgUser->addWatch( $this->getTitle() );
638 }
639 return true;
640
641 }
642
643 /**
644 * Record a file upload in the upload log and the image table
645 */
646 function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false )
647 {
648 global $wgUser;
649
650 $dbw = $this->repo->getMasterDB();
651
652 if ( !$props ) {
653 $props = $this->repo->getFileProps( $this->getVirtualUrl() );
654 }
655 $this->setProps( $props );
656
657 // Delete thumbnails and refresh the metadata cache
658 $this->purgeThumbnails();
659 $this->saveToCache();
660 wfPurgeSquidServers( array( $this->getURL() ) );
661
662 // Fail now if the file isn't there
663 if ( !$this->fileExists ) {
664 wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" );
665 return false;
666 }
667
668 if ( $timestamp === false ) {
669 $timestamp = $dbw->timestamp();
670 }
671
672 # Test to see if the row exists using INSERT IGNORE
673 # This avoids race conditions by locking the row until the commit, and also
674 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
675 $dbw->insert( 'image',
676 array(
677 'img_name' => $this->getName(),
678 'img_size'=> $this->size,
679 'img_width' => intval( $this->width ),
680 'img_height' => intval( $this->height ),
681 'img_bits' => $this->bits,
682 'img_media_type' => $this->media_type,
683 'img_major_mime' => $this->major_mime,
684 'img_minor_mime' => $this->minor_mime,
685 'img_timestamp' => $timestamp,
686 'img_description' => $comment,
687 'img_user' => $wgUser->getID(),
688 'img_user_text' => $wgUser->getName(),
689 'img_metadata' => $this->metadata,
690 ),
691 __METHOD__,
692 'IGNORE'
693 );
694
695 if( $dbw->affectedRows() == 0 ) {
696 # Collision, this is an update of a file
697 # Insert previous contents into oldimage
698 $dbw->insertSelect( 'oldimage', 'image',
699 array(
700 'oi_name' => 'img_name',
701 'oi_archive_name' => $dbw->addQuotes( $oldver ),
702 'oi_size' => 'img_size',
703 'oi_width' => 'img_width',
704 'oi_height' => 'img_height',
705 'oi_bits' => 'img_bits',
706 'oi_timestamp' => 'img_timestamp',
707 'oi_description' => 'img_description',
708 'oi_user' => 'img_user',
709 'oi_user_text' => 'img_user_text',
710 ), array( 'img_name' => $this->getName() ), __METHOD__
711 );
712
713 # Update the current image row
714 $dbw->update( 'image',
715 array( /* SET */
716 'img_size' => $this->size,
717 'img_width' => intval( $this->width ),
718 'img_height' => intval( $this->height ),
719 'img_bits' => $this->bits,
720 'img_media_type' => $this->media_type,
721 'img_major_mime' => $this->major_mime,
722 'img_minor_mime' => $this->minor_mime,
723 'img_timestamp' => $timestamp,
724 'img_description' => $comment,
725 'img_user' => $wgUser->getID(),
726 'img_user_text' => $wgUser->getName(),
727 'img_metadata' => $this->metadata,
728 ), array( /* WHERE */
729 'img_name' => $this->getName()
730 ), __METHOD__
731 );
732 } else {
733 # This is a new file
734 # Update the image count
735 $site_stats = $dbw->tableName( 'site_stats' );
736 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
737 }
738
739 $descTitle = $this->getTitle();
740 $article = new Article( $descTitle );
741
742 # Add the log entry
743 $log = new LogPage( 'upload' );
744 $log->addEntry( 'upload', $descTitle, $comment );
745
746 if( $descTitle->exists() ) {
747 # Create a null revision
748 $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false );
749 $nullRevision->insertOn( $dbw );
750
751 # Invalidate the cache for the description page
752 $descTitle->invalidateCache();
753 $descTitle->purgeSquid();
754 } else {
755 // New file; create the description page.
756 // There's already a log entry, so don't make a second RC entry
757 $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
758 }
759
760 # Hooks, hooks, the magic of hooks...
761 wfRunHooks( 'FileUpload', array( $this ) );
762
763 # Commit the transaction now, in case something goes wrong later
764 # The most important thing is that files don't get lost, especially archives
765 $dbw->immediateCommit();
766
767 # Invalidate cache for all pages using this file
768 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
769 $update->doUpdate();
770
771 return true;
772 }
773
774 /**
775 * Move or copy a file to its public location. If a file exists at the
776 * destination, move it to an archive. Returns the archive name on success
777 * or an empty string if it was a new file, and a wikitext-formatted
778 * WikiError object on failure.
779 *
780 * The archive name should be passed through to recordUpload for database
781 * registration.
782 *
783 * @param string $sourcePath Local filesystem path to the source image
784 * @param integer $flags A bitwise combination of:
785 * File::DELETE_SOURCE Delete the source file, i.e. move
786 * rather than copy
787 * @return The archive name on success or an empty string if it was a new
788 * file, and a wikitext-formatted WikiError object on failure.
789 */
790 function publish( $srcPath, $flags = 0 ) {
791 $dstRel = $this->getRel();
792 $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
793 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
794 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
795 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
796 if ( WikiError::isError( $status ) ) {
797 return $status;
798 } elseif ( $status == 'new' ) {
799 return '';
800 } else {
801 return $archiveName;
802 }
803 }
804
805 /** getLinksTo inherited */
806 /** getExifData inherited */
807 /** isLocal inherited */
808 /** wasDeleted inherited */
809
810 /**
811 * Delete all versions of the file.
812 *
813 * Moves the files into an archive directory (or deletes them)
814 * and removes the database rows.
815 *
816 * Cache purging is done; logging is caller's responsibility.
817 *
818 * @param $reason
819 * @return true on success, false on some kind of failure
820 */
821 function delete( $reason, $suppress=false ) {
822 $transaction = new FSTransaction();
823 $urlArr = array( $this->getURL() );
824
825 if( !FileStore::lock() ) {
826 wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
827 return false;
828 }
829
830 try {
831 $dbw = $this->repo->getMasterDB();
832 $dbw->begin();
833
834 // Delete old versions
835 $result = $dbw->select( 'oldimage',
836 array( 'oi_archive_name' ),
837 array( 'oi_name' => $this->getName() ) );
838
839 while( $row = $dbw->fetchObject( $result ) ) {
840 $oldName = $row->oi_archive_name;
841
842 $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) );
843
844 // We'll need to purge this URL from caches...
845 $urlArr[] = $this->getArchiveUrl( $oldName );
846 }
847 $dbw->freeResult( $result );
848
849 // And the current version...
850 $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) );
851
852 $dbw->immediateCommit();
853 } catch( MWException $e ) {
854 wfDebug( __METHOD__.": db error, rolling back file transactions\n" );
855 $transaction->rollback();
856 FileStore::unlock();
857 throw $e;
858 }
859
860 wfDebug( __METHOD__.": deleted db items, applying file transactions\n" );
861 $transaction->commit();
862 FileStore::unlock();
863
864
865 // Update site_stats
866 $site_stats = $dbw->tableName( 'site_stats' );
867 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
868
869 $this->purgeEverything( $urlArr );
870
871 return true;
872 }
873
874
875 /**
876 * Delete an old version of the file.
877 *
878 * Moves the file into an archive directory (or deletes it)
879 * and removes the database row.
880 *
881 * Cache purging is done; logging is caller's responsibility.
882 *
883 * @param $reason
884 * @throws MWException or FSException on database or filestore failure
885 * @return true on success, false on some kind of failure
886 */
887 function deleteOld( $archiveName, $reason, $suppress=false ) {
888 $transaction = new FSTransaction();
889 $urlArr = array();
890
891 if( !FileStore::lock() ) {
892 wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
893 return false;
894 }
895
896 $transaction = new FSTransaction();
897 try {
898 $dbw = $this->repo->getMasterDB();
899 $dbw->begin();
900 $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) );
901 $dbw->immediateCommit();
902 } catch( MWException $e ) {
903 wfDebug( __METHOD__.": db error, rolling back file transaction\n" );
904 $transaction->rollback();
905 FileStore::unlock();
906 throw $e;
907 }
908
909 wfDebug( __METHOD__.": deleted db items, applying file transaction\n" );
910 $transaction->commit();
911 FileStore::unlock();
912
913 $this->purgeDescription();
914
915 // Squid purging
916 global $wgUseSquid;
917 if ( $wgUseSquid ) {
918 $urlArr = array(
919 $this->getArchiveUrl( $archiveName ),
920 );
921 wfPurgeSquidServers( $urlArr );
922 }
923 return true;
924 }
925
926 /**
927 * Delete the current version of a file.
928 * May throw a database error.
929 * @return true on success, false on failure
930 */
931 private function prepareDeleteCurrent( $reason, $suppress=false ) {
932 return $this->prepareDeleteVersion(
933 $this->getFullPath(),
934 $reason,
935 'image',
936 array(
937 'fa_name' => 'img_name',
938 'fa_archive_name' => 'NULL',
939 'fa_size' => 'img_size',
940 'fa_width' => 'img_width',
941 'fa_height' => 'img_height',
942 'fa_metadata' => 'img_metadata',
943 'fa_bits' => 'img_bits',
944 'fa_media_type' => 'img_media_type',
945 'fa_major_mime' => 'img_major_mime',
946 'fa_minor_mime' => 'img_minor_mime',
947 'fa_description' => 'img_description',
948 'fa_user' => 'img_user',
949 'fa_user_text' => 'img_user_text',
950 'fa_timestamp' => 'img_timestamp' ),
951 array( 'img_name' => $this->getName() ),
952 $suppress,
953 __METHOD__ );
954 }
955
956 /**
957 * Delete a given older version of a file.
958 * May throw a database error.
959 * @return true on success, false on failure
960 */
961 private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) {
962 $oldpath = $this->getArchivePath() .
963 DIRECTORY_SEPARATOR . $archiveName;
964 return $this->prepareDeleteVersion(
965 $oldpath,
966 $reason,
967 'oldimage',
968 array(
969 'fa_name' => 'oi_name',
970 'fa_archive_name' => 'oi_archive_name',
971 'fa_size' => 'oi_size',
972 'fa_width' => 'oi_width',
973 'fa_height' => 'oi_height',
974 'fa_metadata' => 'NULL',
975 'fa_bits' => 'oi_bits',
976 'fa_media_type' => 'NULL',
977 'fa_major_mime' => 'NULL',
978 'fa_minor_mime' => 'NULL',
979 'fa_description' => 'oi_description',
980 'fa_user' => 'oi_user',
981 'fa_user_text' => 'oi_user_text',
982 'fa_timestamp' => 'oi_timestamp' ),
983 array(
984 'oi_name' => $this->getName(),
985 'oi_archive_name' => $archiveName ),
986 $suppress,
987 __METHOD__ );
988 }
989
990 /**
991 * Do the dirty work of backing up an image row and its file
992 * (if $wgSaveDeletedFiles is on) and removing the originals.
993 *
994 * Must be run while the file store is locked and a database
995 * transaction is open to avoid race conditions.
996 *
997 * @return FSTransaction
998 */
999 private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) {
1000 global $wgUser, $wgSaveDeletedFiles;
1001
1002 // Dupe the file into the file store
1003 if( file_exists( $path ) ) {
1004 if( $wgSaveDeletedFiles ) {
1005 $group = 'deleted';
1006
1007 $store = FileStore::get( $group );
1008 $key = FileStore::calculateKey( $path, $this->getExtension() );
1009 $transaction = $store->insert( $key, $path,
1010 FileStore::DELETE_ORIGINAL );
1011 } else {
1012 $group = null;
1013 $key = null;
1014 $transaction = FileStore::deleteFile( $path );
1015 }
1016 } else {
1017 wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" );
1018 $group = null;
1019 $key = null;
1020 $transaction = new FSTransaction(); // empty
1021 }
1022
1023 if( $transaction === false ) {
1024 // Fail to restore?
1025 wfDebug( __METHOD__.": import to file store failed, aborting\n" );
1026 throw new MWException( "Could not archive and delete file $path" );
1027 return false;
1028 }
1029
1030 // Bitfields to further supress the file content
1031 // Note that currently, live files are stored elsewhere
1032 // and cannot be partially deleted
1033 $bitfield = 0;
1034 if ( $suppress ) {
1035 $bitfield |= self::DELETED_FILE;
1036 $bitfield |= self::DELETED_COMMENT;
1037 $bitfield |= self::DELETED_USER;
1038 $bitfield |= self::DELETED_RESTRICTED;
1039 }
1040
1041 $dbw = $this->repo->getMasterDB();
1042 $storageMap = array(
1043 'fa_storage_group' => $dbw->addQuotes( $group ),
1044 'fa_storage_key' => $dbw->addQuotes( $key ),
1045
1046 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ),
1047 'fa_deleted_timestamp' => $dbw->timestamp(),
1048 'fa_deleted_reason' => $dbw->addQuotes( $reason ),
1049 'fa_deleted' => $bitfield);
1050 $allFields = array_merge( $storageMap, $fieldMap );
1051
1052 try {
1053 if( $wgSaveDeletedFiles ) {
1054 $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
1055 }
1056 $dbw->delete( $table, $where, $fname );
1057 } catch( DBQueryError $e ) {
1058 // Something went horribly wrong!
1059 // Leave the file as it was...
1060 wfDebug( __METHOD__.": database error, rolling back file transaction\n" );
1061 $transaction->rollback();
1062 throw $e;
1063 }
1064
1065 return $transaction;
1066 }
1067
1068 /**
1069 * Restore all or specified deleted revisions to the given file.
1070 * Permissions and logging are left to the caller.
1071 *
1072 * May throw database exceptions on error.
1073 *
1074 * @param $versions set of record ids of deleted items to restore,
1075 * or empty to restore all revisions.
1076 * @return the number of file revisions restored if successful,
1077 * or false on failure
1078 */
1079 function restore( $versions=array(), $Unsuppress=false ) {
1080 global $wgUser;
1081
1082 if( !FileStore::lock() ) {
1083 wfDebug( __METHOD__." could not acquire filestore lock\n" );
1084 return false;
1085 }
1086
1087 $transaction = new FSTransaction();
1088 try {
1089 $dbw = $this->repo->getMasterDB();
1090 $dbw->begin();
1091
1092 // Re-confirm whether this file presently exists;
1093 // if no we'll need to create an file record for the
1094 // first item we restore.
1095 $exists = $dbw->selectField( 'image', '1',
1096 array( 'img_name' => $this->getName() ),
1097 __METHOD__ );
1098
1099 // Fetch all or selected archived revisions for the file,
1100 // sorted from the most recent to the oldest.
1101 $conditions = array( 'fa_name' => $this->getName() );
1102 if( $versions ) {
1103 $conditions['fa_id'] = $versions;
1104 }
1105
1106 $result = $dbw->select( 'filearchive', '*',
1107 $conditions,
1108 __METHOD__,
1109 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
1110
1111 if( $dbw->numRows( $result ) < count( $versions ) ) {
1112 // There's some kind of conflict or confusion;
1113 // we can't restore everything we were asked to.
1114 wfDebug( __METHOD__.": couldn't find requested items\n" );
1115 $dbw->rollback();
1116 FileStore::unlock();
1117 return false;
1118 }
1119
1120 if( $dbw->numRows( $result ) == 0 ) {
1121 // Nothing to do.
1122 wfDebug( __METHOD__.": nothing to do\n" );
1123 $dbw->rollback();
1124 FileStore::unlock();
1125 return true;
1126 }
1127
1128 $revisions = 0;
1129 while( $row = $dbw->fetchObject( $result ) ) {
1130 if ( $Unsuppress ) {
1131 // Currently, fa_deleted flags fall off upon restore, lets be careful about this
1132 } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
1133 // Skip restoring file revisions that the user cannot restore
1134 continue;
1135 }
1136 $revisions++;
1137 $store = FileStore::get( $row->fa_storage_group );
1138 if( !$store ) {
1139 wfDebug( __METHOD__.": skipping row with no file.\n" );
1140 continue;
1141 }
1142
1143 $restoredImage = new self( Title::makeTitle( NS_IMAGE, $row->fa_name ), $this->repo );
1144
1145 if( $revisions == 1 && !$exists ) {
1146 $destPath = $restoredImage->getFullPath();
1147 $destDir = dirname( $destPath );
1148 if ( !is_dir( $destDir ) ) {
1149 wfMkdirParents( $destDir );
1150 }
1151
1152 // We may have to fill in data if this was originally
1153 // an archived file revision.
1154 if( is_null( $row->fa_metadata ) ) {
1155 $tempFile = $store->filePath( $row->fa_storage_key );
1156
1157 $magic = MimeMagic::singleton();
1158 $mime = $magic->guessMimeType( $tempFile, true );
1159 $media_type = $magic->getMediaType( $tempFile, $mime );
1160 list( $major_mime, $minor_mime ) = self::splitMime( $mime );
1161 $handler = MediaHandler::getHandler( $mime );
1162 if ( $handler ) {
1163 $metadata = $handler->getMetadata( false, $tempFile );
1164 } else {
1165 $metadata = '';
1166 }
1167 } else {
1168 $metadata = $row->fa_metadata;
1169 $major_mime = $row->fa_major_mime;
1170 $minor_mime = $row->fa_minor_mime;
1171 $media_type = $row->fa_media_type;
1172 }
1173
1174 $table = 'image';
1175 $fields = array(
1176 'img_name' => $row->fa_name,
1177 'img_size' => $row->fa_size,
1178 'img_width' => $row->fa_width,
1179 'img_height' => $row->fa_height,
1180 'img_metadata' => $metadata,
1181 'img_bits' => $row->fa_bits,
1182 'img_media_type' => $media_type,
1183 'img_major_mime' => $major_mime,
1184 'img_minor_mime' => $minor_mime,
1185 'img_description' => $row->fa_description,
1186 'img_user' => $row->fa_user,
1187 'img_user_text' => $row->fa_user_text,
1188 'img_timestamp' => $row->fa_timestamp );
1189 } else {
1190 $archiveName = $row->fa_archive_name;
1191 if( $archiveName == '' ) {
1192 // This was originally a current version; we
1193 // have to devise a new archive name for it.
1194 // Format is <timestamp of archiving>!<name>
1195 $archiveName =
1196 wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) .
1197 '!' . $row->fa_name;
1198 }
1199 $destDir = $restoredImage->getArchivePath();
1200 if ( !is_dir( $destDir ) ) {
1201 wfMkdirParents( $destDir );
1202 }
1203 $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName;
1204
1205 $table = 'oldimage';
1206 $fields = array(
1207 'oi_name' => $row->fa_name,
1208 'oi_archive_name' => $archiveName,
1209 'oi_size' => $row->fa_size,
1210 'oi_width' => $row->fa_width,
1211 'oi_height' => $row->fa_height,
1212 'oi_bits' => $row->fa_bits,
1213 'oi_description' => $row->fa_description,
1214 'oi_user' => $row->fa_user,
1215 'oi_user_text' => $row->fa_user_text,
1216 'oi_timestamp' => $row->fa_timestamp );
1217 }
1218
1219 $dbw->insert( $table, $fields, __METHOD__ );
1220 // @todo this delete is not totally safe, potentially
1221 $dbw->delete( 'filearchive',
1222 array( 'fa_id' => $row->fa_id ),
1223 __METHOD__ );
1224
1225 // Check if any other stored revisions use this file;
1226 // if so, we shouldn't remove the file from the deletion
1227 // archives so they will still work.
1228 $useCount = $dbw->selectField( 'filearchive',
1229 'COUNT(*)',
1230 array(
1231 'fa_storage_group' => $row->fa_storage_group,
1232 'fa_storage_key' => $row->fa_storage_key ),
1233 __METHOD__ );
1234 if( $useCount == 0 ) {
1235 wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
1236 $flags = FileStore::DELETE_ORIGINAL;
1237 } else {
1238 $flags = 0;
1239 }
1240
1241 $transaction->add( $store->export( $row->fa_storage_key,
1242 $destPath, $flags ) );
1243 }
1244
1245 $dbw->immediateCommit();
1246 } catch( MWException $e ) {
1247 wfDebug( __METHOD__." caught error, aborting\n" );
1248 $transaction->rollback();
1249 $dbw->rollback();
1250 throw $e;
1251 }
1252
1253 $transaction->commit();
1254 FileStore::unlock();
1255
1256 if( $revisions > 0 ) {
1257 if( !$exists ) {
1258 wfDebug( __METHOD__." restored $revisions items, creating a new current\n" );
1259
1260 // Update site_stats
1261 $site_stats = $dbw->tableName( 'site_stats' );
1262 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1263
1264 $this->purgeEverything();
1265 } else {
1266 wfDebug( __METHOD__." restored $revisions as archived versions\n" );
1267 $this->purgeDescription();
1268 }
1269 }
1270
1271 return $revisions;
1272 }
1273
1274 /** isMultipage inherited */
1275 /** pageCount inherited */
1276 /** scaleHeight inherited */
1277 /** getImageSize inherited */
1278
1279 /**
1280 * Get the URL of the file description page.
1281 */
1282 function getDescriptionUrl() {
1283 return $this->title->getLocalUrl();
1284 }
1285
1286 /**
1287 * Get the HTML text of the description page
1288 * This is not used by ImagePage for local files, since (among other things)
1289 * it skips the parser cache.
1290 */
1291 function getDescriptionText() {
1292 global $wgParser;
1293 $revision = Revision::newFromTitle( $this->title );
1294 if ( !$revision ) return false;
1295 $text = $revision->getText();
1296 if ( !$text ) return false;
1297 $html = $wgParser->parse( $text, new ParserOptions );
1298 return $html;
1299 }
1300
1301 function getTimestamp() {
1302 $this->load();
1303 return $this->timestamp;
1304 }
1305 } // LocalFile class
1306
1307 /**
1308 * Backwards compatibility class
1309 */
1310 class Image extends LocalFile {
1311 function __construct( $title ) {
1312 $repo = RepoGroup::singleton()->getLocalRepo();
1313 parent::__construct( $title, $repo );
1314 }
1315
1316 /**
1317 * Wrapper for wfFindFile(), for backwards-compatibility only
1318 * Do not use in core code.
1319 * @deprecated
1320 */
1321 static function newFromTitle( $title, $time = false ) {
1322 $img = wfFindFile( $title, $time );
1323 if ( !$img ) {
1324 $img = wfLocalFile( $title );
1325 }
1326 return $img;
1327 }
1328
1329 /**
1330 * Wrapper for wfFindFile(), for backwards-compatibility only.
1331 * Do not use in core code.
1332 *
1333 * @param string $name name of the image, used to create a title object using Title::makeTitleSafe
1334 * @return image object or null if invalid title
1335 * @deprecated
1336 */
1337 static function newFromName( $name ) {
1338 $title = Title::makeTitleSafe( NS_IMAGE, $name );
1339 if ( is_object( $title ) ) {
1340 $img = wfFindFile( $title );
1341 if ( !$img ) {
1342 $img = wfLocalFile( $title );
1343 }
1344 return $img;
1345 } else {
1346 return NULL;
1347 }
1348 }
1349
1350 /**
1351 * Return the URL of an image, provided its name.
1352 *
1353 * Backwards-compatibility for extensions.
1354 * Note that fromSharedDirectory will only use the shared path for files
1355 * that actually exist there now, and will return local paths otherwise.
1356 *
1357 * @param string $name Name of the image, without the leading "Image:"
1358 * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath?
1359 * @return string URL of $name image
1360 * @deprecated
1361 */
1362 static function imageUrl( $name, $fromSharedDirectory = false ) {
1363 $image = null;
1364 if( $fromSharedDirectory ) {
1365 $image = wfFindFile( $name );
1366 }
1367 if( !$image ) {
1368 $image = wfLocalFile( $name );
1369 }
1370 return $image->getUrl();
1371 }
1372 }
1373
1374 /**
1375 * Aliases for backwards compatibility with 1.6
1376 */
1377 define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
1378 define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
1379 define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
1380 define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
1381
1382