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