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