[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/filerepo/file/ -> LocalFile.php (source)

   1  <?php
   2  /**
   3   * Local file in the wiki's own database.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup FileAbstraction
  22   */
  23  
  24  /**
  25   * Bump this number when serialized cache records may be incompatible.
  26   */
  27  define( 'MW_FILE_VERSION', 9 );
  28  
  29  /**
  30   * Class to represent a local file in the wiki's own database
  31   *
  32   * Provides methods to retrieve paths (physical, logical, URL),
  33   * to generate image thumbnails or for uploading.
  34   *
  35   * Note that only the repo object knows what its file class is called. You should
  36   * never name a file class explictly outside of the repo class. Instead use the
  37   * repo's factory functions to generate file objects, for example:
  38   *
  39   * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
  40   *
  41   * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
  42   * in most cases.
  43   *
  44   * @ingroup FileAbstraction
  45   */
  46  class LocalFile extends File {
  47      const CACHE_FIELD_MAX_LEN = 1000;
  48  
  49      /** @var bool Does the file exist on disk? (loadFromXxx) */
  50      protected $fileExists;
  51  
  52      /** @var int Image width */
  53      protected $width;
  54  
  55      /** @var int Image height */
  56      protected $height;
  57  
  58      /** @var int Returned by getimagesize (loadFromXxx) */
  59      protected $bits;
  60  
  61      /** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
  62      protected $media_type;
  63  
  64      /** @var string MIME type, determined by MimeMagic::guessMimeType */
  65      protected $mime;
  66  
  67      /** @var int Size in bytes (loadFromXxx) */
  68      protected $size;
  69  
  70      /** @var string Handler-specific metadata */
  71      protected $metadata;
  72  
  73      /** @var string SHA-1 base 36 content hash */
  74      protected $sha1;
  75  
  76      /** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
  77      protected $dataLoaded;
  78  
  79      /** @var bool Whether or not lazy-loaded data has been loaded from the database */
  80      protected $extraDataLoaded;
  81  
  82      /** @var int Bitfield akin to rev_deleted */
  83      protected $deleted;
  84  
  85      /** @var string */
  86      protected $repoClass = 'LocalRepo';
  87  
  88      /** @var int Number of line to return by nextHistoryLine() (constructor) */
  89      private $historyLine;
  90  
  91      /** @var int Result of the query for the file's history (nextHistoryLine) */
  92      private $historyRes;
  93  
  94      /** @var string Major MIME type */
  95      private $major_mime;
  96  
  97      /** @var string Minor MIME type */
  98      private $minor_mime;
  99  
 100      /** @var string Upload timestamp */
 101      private $timestamp;
 102  
 103      /** @var int User ID of uploader */
 104      private $user;
 105  
 106      /** @var string User name of uploader */
 107      private $user_text;
 108  
 109      /** @var string Description of current revision of the file */
 110      private $description;
 111  
 112      /** @var bool Whether the row was upgraded on load */
 113      private $upgraded;
 114  
 115      /** @var bool True if the image row is locked */
 116      private $locked;
 117  
 118      /** @var bool True if the image row is locked with a lock initiated transaction */
 119      private $lockedOwnTrx;
 120  
 121      /** @var bool True if file is not present in file system. Not to be cached in memcached */
 122      private $missing;
 123  
 124      /** @var int UNIX timestamp of last markVolatile() call */
 125      private $lastMarkedVolatile = 0;
 126  
 127      const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
 128      const LOAD_VIA_SLAVE = 2; // integer; use a slave to load the data
 129  
 130      const VOLATILE_TTL = 300; // integer; seconds
 131  
 132      /**
 133       * Create a LocalFile from a title
 134       * Do not call this except from inside a repo class.
 135       *
 136       * Note: $unused param is only here to avoid an E_STRICT
 137       *
 138       * @param Title $title
 139       * @param FileRepo $repo
 140       * @param null $unused
 141       *
 142       * @return LocalFile
 143       */
 144  	static function newFromTitle( $title, $repo, $unused = null ) {
 145          return new self( $title, $repo );
 146      }
 147  
 148      /**
 149       * Create a LocalFile from a title
 150       * Do not call this except from inside a repo class.
 151       *
 152       * @param stdClass $row
 153       * @param FileRepo $repo
 154       *
 155       * @return LocalFile
 156       */
 157  	static function newFromRow( $row, $repo ) {
 158          $title = Title::makeTitle( NS_FILE, $row->img_name );
 159          $file = new self( $title, $repo );
 160          $file->loadFromRow( $row );
 161  
 162          return $file;
 163      }
 164  
 165      /**
 166       * Create a LocalFile from a SHA-1 key
 167       * Do not call this except from inside a repo class.
 168       *
 169       * @param string $sha1 Base-36 SHA-1
 170       * @param LocalRepo $repo
 171       * @param string|bool $timestamp MW_timestamp (optional)
 172       * @return bool|LocalFile
 173       */
 174  	static function newFromKey( $sha1, $repo, $timestamp = false ) {
 175          $dbr = $repo->getSlaveDB();
 176  
 177          $conds = array( 'img_sha1' => $sha1 );
 178          if ( $timestamp ) {
 179              $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
 180          }
 181  
 182          $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
 183          if ( $row ) {
 184              return self::newFromRow( $row, $repo );
 185          } else {
 186              return false;
 187          }
 188      }
 189  
 190      /**
 191       * Fields in the image table
 192       * @return array
 193       */
 194  	static function selectFields() {
 195          return array(
 196              'img_name',
 197              'img_size',
 198              'img_width',
 199              'img_height',
 200              'img_metadata',
 201              'img_bits',
 202              'img_media_type',
 203              'img_major_mime',
 204              'img_minor_mime',
 205              'img_description',
 206              'img_user',
 207              'img_user_text',
 208              'img_timestamp',
 209              'img_sha1',
 210          );
 211      }
 212  
 213      /**
 214       * Constructor.
 215       * Do not call this except from inside a repo class.
 216       * @param Title $title
 217       * @param FileRepo $repo
 218       */
 219  	function __construct( $title, $repo ) {
 220          parent::__construct( $title, $repo );
 221  
 222          $this->metadata = '';
 223          $this->historyLine = 0;
 224          $this->historyRes = null;
 225          $this->dataLoaded = false;
 226          $this->extraDataLoaded = false;
 227  
 228          $this->assertRepoDefined();
 229          $this->assertTitleDefined();
 230      }
 231  
 232      /**
 233       * Get the memcached key for the main data for this file, or false if
 234       * there is no access to the shared cache.
 235       * @return string|bool
 236       */
 237  	function getCacheKey() {
 238          $hashedName = md5( $this->getName() );
 239  
 240          return $this->repo->getSharedCacheKey( 'file', $hashedName );
 241      }
 242  
 243      /**
 244       * Try to load file metadata from memcached. Returns true on success.
 245       * @return bool
 246       */
 247  	function loadFromCache() {
 248          global $wgMemc;
 249  
 250          wfProfileIn( __METHOD__ );
 251          $this->dataLoaded = false;
 252          $this->extraDataLoaded = false;
 253          $key = $this->getCacheKey();
 254  
 255          if ( !$key ) {
 256              wfProfileOut( __METHOD__ );
 257  
 258              return false;
 259          }
 260  
 261          $cachedValues = $wgMemc->get( $key );
 262  
 263          // Check if the key existed and belongs to this version of MediaWiki
 264          if ( isset( $cachedValues['version'] ) && $cachedValues['version'] == MW_FILE_VERSION ) {
 265              wfDebug( "Pulling file metadata from cache key $key\n" );
 266              $this->fileExists = $cachedValues['fileExists'];
 267              if ( $this->fileExists ) {
 268                  $this->setProps( $cachedValues );
 269              }
 270              $this->dataLoaded = true;
 271              $this->extraDataLoaded = true;
 272              foreach ( $this->getLazyCacheFields( '' ) as $field ) {
 273                  $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
 274              }
 275          }
 276  
 277          if ( $this->dataLoaded ) {
 278              wfIncrStats( 'image_cache_hit' );
 279          } else {
 280              wfIncrStats( 'image_cache_miss' );
 281          }
 282  
 283          wfProfileOut( __METHOD__ );
 284  
 285          return $this->dataLoaded;
 286      }
 287  
 288      /**
 289       * Save the file metadata to memcached
 290       */
 291  	function saveToCache() {
 292          global $wgMemc;
 293  
 294          $this->load();
 295          $key = $this->getCacheKey();
 296  
 297          if ( !$key ) {
 298              return;
 299          }
 300  
 301          $fields = $this->getCacheFields( '' );
 302          $cache = array( 'version' => MW_FILE_VERSION );
 303          $cache['fileExists'] = $this->fileExists;
 304  
 305          if ( $this->fileExists ) {
 306              foreach ( $fields as $field ) {
 307                  $cache[$field] = $this->$field;
 308              }
 309          }
 310  
 311          // Strip off excessive entries from the subset of fields that can become large.
 312          // If the cache value gets to large it will not fit in memcached and nothing will
 313          // get cached at all, causing master queries for any file access.
 314          foreach ( $this->getLazyCacheFields( '' ) as $field ) {
 315              if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) {
 316                  unset( $cache[$field] ); // don't let the value get too big
 317              }
 318          }
 319  
 320          // Cache presence for 1 week and negatives for 1 day
 321          $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 );
 322      }
 323  
 324      /**
 325       * Load metadata from the file itself
 326       */
 327  	function loadFromFile() {
 328          $props = $this->repo->getFileProps( $this->getVirtualUrl() );
 329          $this->setProps( $props );
 330      }
 331  
 332      /**
 333       * @param string $prefix
 334       * @return array
 335       */
 336  	function getCacheFields( $prefix = 'img_' ) {
 337          static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
 338              'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
 339              'user_text', 'description' );
 340          static $results = array();
 341  
 342          if ( $prefix == '' ) {
 343              return $fields;
 344          }
 345  
 346          if ( !isset( $results[$prefix] ) ) {
 347              $prefixedFields = array();
 348              foreach ( $fields as $field ) {
 349                  $prefixedFields[] = $prefix . $field;
 350              }
 351              $results[$prefix] = $prefixedFields;
 352          }
 353  
 354          return $results[$prefix];
 355      }
 356  
 357      /**
 358       * @param string $prefix
 359       * @return array
 360       */
 361  	function getLazyCacheFields( $prefix = 'img_' ) {
 362          static $fields = array( 'metadata' );
 363          static $results = array();
 364  
 365          if ( $prefix == '' ) {
 366              return $fields;
 367          }
 368  
 369          if ( !isset( $results[$prefix] ) ) {
 370              $prefixedFields = array();
 371              foreach ( $fields as $field ) {
 372                  $prefixedFields[] = $prefix . $field;
 373              }
 374              $results[$prefix] = $prefixedFields;
 375          }
 376  
 377          return $results[$prefix];
 378      }
 379  
 380      /**
 381       * Load file metadata from the DB
 382       * @param int $flags
 383       */
 384  	function loadFromDB( $flags = 0 ) {
 385          # Polymorphic function name to distinguish foreign and local fetches
 386          $fname = get_class( $this ) . '::' . __FUNCTION__;
 387          wfProfileIn( $fname );
 388  
 389          # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
 390          $this->dataLoaded = true;
 391          $this->extraDataLoaded = true;
 392  
 393          $dbr = ( $flags & self::LOAD_VIA_SLAVE )
 394              ? $this->repo->getSlaveDB()
 395              : $this->repo->getMasterDB();
 396  
 397          $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
 398              array( 'img_name' => $this->getName() ), $fname );
 399  
 400          if ( $row ) {
 401              $this->loadFromRow( $row );
 402          } else {
 403              $this->fileExists = false;
 404          }
 405  
 406          wfProfileOut( $fname );
 407      }
 408  
 409      /**
 410       * Load lazy file metadata from the DB.
 411       * This covers fields that are sometimes not cached.
 412       */
 413  	protected function loadExtraFromDB() {
 414          # Polymorphic function name to distinguish foreign and local fetches
 415          $fname = get_class( $this ) . '::' . __FUNCTION__;
 416          wfProfileIn( $fname );
 417  
 418          # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
 419          $this->extraDataLoaded = true;
 420  
 421          $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname );
 422          if ( !$fieldMap ) {
 423              $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
 424          }
 425  
 426          if ( $fieldMap ) {
 427              foreach ( $fieldMap as $name => $value ) {
 428                  $this->$name = $value;
 429              }
 430          } else {
 431              wfProfileOut( $fname );
 432              throw new MWException( "Could not find data for image '{$this->getName()}'." );
 433          }
 434  
 435          wfProfileOut( $fname );
 436      }
 437  
 438      /**
 439       * @param DatabaseBase $dbr
 440       * @param string $fname
 441       * @return array|bool
 442       */
 443  	private function loadFieldsWithTimestamp( $dbr, $fname ) {
 444          $fieldMap = false;
 445  
 446          $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
 447              array( 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ),
 448              $fname );
 449          if ( $row ) {
 450              $fieldMap = $this->unprefixRow( $row, 'img_' );
 451          } else {
 452              # File may have been uploaded over in the meantime; check the old versions
 453              $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
 454                  array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ),
 455                  $fname );
 456              if ( $row ) {
 457                  $fieldMap = $this->unprefixRow( $row, 'oi_' );
 458              }
 459          }
 460  
 461          return $fieldMap;
 462      }
 463  
 464      /**
 465       * @param array $row Row
 466       * @param string $prefix
 467       * @throws MWException
 468       * @return array
 469       */
 470  	protected function unprefixRow( $row, $prefix = 'img_' ) {
 471          $array = (array)$row;
 472          $prefixLength = strlen( $prefix );
 473  
 474          // Sanity check prefix once
 475          if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
 476              throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
 477          }
 478  
 479          $decoded = array();
 480          foreach ( $array as $name => $value ) {
 481              $decoded[substr( $name, $prefixLength )] = $value;
 482          }
 483  
 484          return $decoded;
 485      }
 486  
 487      /**
 488       * Decode a row from the database (either object or array) to an array
 489       * with timestamps and MIME types decoded, and the field prefix removed.
 490       * @param object $row
 491       * @param string $prefix
 492       * @throws MWException
 493       * @return array
 494       */
 495  	function decodeRow( $row, $prefix = 'img_' ) {
 496          $decoded = $this->unprefixRow( $row, $prefix );
 497  
 498          $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
 499  
 500          $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
 501  
 502          if ( empty( $decoded['major_mime'] ) ) {
 503              $decoded['mime'] = 'unknown/unknown';
 504          } else {
 505              if ( !$decoded['minor_mime'] ) {
 506                  $decoded['minor_mime'] = 'unknown';
 507              }
 508              $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
 509          }
 510  
 511          # Trim zero padding from char/binary field
 512          $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
 513  
 514          return $decoded;
 515      }
 516  
 517      /**
 518       * Load file metadata from a DB result row
 519       *
 520       * @param object $row
 521       * @param string $prefix
 522       */
 523  	function loadFromRow( $row, $prefix = 'img_' ) {
 524          $this->dataLoaded = true;
 525          $this->extraDataLoaded = true;
 526  
 527          $array = $this->decodeRow( $row, $prefix );
 528  
 529          foreach ( $array as $name => $value ) {
 530              $this->$name = $value;
 531          }
 532  
 533          $this->fileExists = true;
 534          $this->maybeUpgradeRow();
 535      }
 536  
 537      /**
 538       * Load file metadata from cache or DB, unless already loaded
 539       * @param int $flags
 540       */
 541  	function load( $flags = 0 ) {
 542          if ( !$this->dataLoaded ) {
 543              if ( !$this->loadFromCache() ) {
 544                  $this->loadFromDB( $this->isVolatile() ? 0 : self::LOAD_VIA_SLAVE );
 545                  $this->saveToCache();
 546              }
 547              $this->dataLoaded = true;
 548          }
 549          if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
 550              $this->loadExtraFromDB();
 551          }
 552      }
 553  
 554      /**
 555       * Upgrade a row if it needs it
 556       */
 557  	function maybeUpgradeRow() {
 558          global $wgUpdateCompatibleMetadata;
 559          if ( wfReadOnly() ) {
 560              return;
 561          }
 562  
 563          if ( is_null( $this->media_type ) ||
 564              $this->mime == 'image/svg'
 565          ) {
 566              $this->upgradeRow();
 567              $this->upgraded = true;
 568          } else {
 569              $handler = $this->getHandler();
 570              if ( $handler ) {
 571                  $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
 572                  if ( $validity === MediaHandler::METADATA_BAD
 573                      || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
 574                  ) {
 575                      $this->upgradeRow();
 576                      $this->upgraded = true;
 577                  }
 578              }
 579          }
 580      }
 581  
 582  	function getUpgraded() {
 583          return $this->upgraded;
 584      }
 585  
 586      /**
 587       * Fix assorted version-related problems with the image row by reloading it from the file
 588       */
 589  	function upgradeRow() {
 590          wfProfileIn( __METHOD__ );
 591  
 592          $this->lock(); // begin
 593  
 594          $this->loadFromFile();
 595  
 596          # Don't destroy file info of missing files
 597          if ( !$this->fileExists ) {
 598              $this->unlock();
 599              wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
 600              wfProfileOut( __METHOD__ );
 601  
 602              return;
 603          }
 604  
 605          $dbw = $this->repo->getMasterDB();
 606          list( $major, $minor ) = self::splitMime( $this->mime );
 607  
 608          if ( wfReadOnly() ) {
 609              $this->unlock();
 610              wfProfileOut( __METHOD__ );
 611  
 612              return;
 613          }
 614          wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
 615  
 616          $dbw->update( 'image',
 617              array(
 618                  'img_size' => $this->size, // sanity
 619                  'img_width' => $this->width,
 620                  'img_height' => $this->height,
 621                  'img_bits' => $this->bits,
 622                  'img_media_type' => $this->media_type,
 623                  'img_major_mime' => $major,
 624                  'img_minor_mime' => $minor,
 625                  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
 626                  'img_sha1' => $this->sha1,
 627              ),
 628              array( 'img_name' => $this->getName() ),
 629              __METHOD__
 630          );
 631  
 632          $this->saveToCache();
 633  
 634          $this->unlock(); // done
 635  
 636          wfProfileOut( __METHOD__ );
 637      }
 638  
 639      /**
 640       * Set properties in this object to be equal to those given in the
 641       * associative array $info. Only cacheable fields can be set.
 642       * All fields *must* be set in $info except for getLazyCacheFields().
 643       *
 644       * If 'mime' is given, it will be split into major_mime/minor_mime.
 645       * If major_mime/minor_mime are given, $this->mime will also be set.
 646       *
 647       * @param array $info
 648       */
 649  	function setProps( $info ) {
 650          $this->dataLoaded = true;
 651          $fields = $this->getCacheFields( '' );
 652          $fields[] = 'fileExists';
 653  
 654          foreach ( $fields as $field ) {
 655              if ( isset( $info[$field] ) ) {
 656                  $this->$field = $info[$field];
 657              }
 658          }
 659  
 660          // Fix up mime fields
 661          if ( isset( $info['major_mime'] ) ) {
 662              $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
 663          } elseif ( isset( $info['mime'] ) ) {
 664              $this->mime = $info['mime'];
 665              list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
 666          }
 667      }
 668  
 669      /** splitMime inherited */
 670      /** getName inherited */
 671      /** getTitle inherited */
 672      /** getURL inherited */
 673      /** getViewURL inherited */
 674      /** getPath inherited */
 675      /** isVisible inhereted */
 676  
 677      /**
 678       * @return bool
 679       */
 680  	function isMissing() {
 681          if ( $this->missing === null ) {
 682              list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
 683              $this->missing = !$fileExists;
 684          }
 685  
 686          return $this->missing;
 687      }
 688  
 689      /**
 690       * Return the width of the image
 691       *
 692       * @param int $page
 693       * @return int
 694       */
 695  	public function getWidth( $page = 1 ) {
 696          $this->load();
 697  
 698          if ( $this->isMultipage() ) {
 699              $handler = $this->getHandler();
 700              if ( !$handler ) {
 701                  return 0;
 702              }
 703              $dim = $handler->getPageDimensions( $this, $page );
 704              if ( $dim ) {
 705                  return $dim['width'];
 706              } else {
 707                  // For non-paged media, the false goes through an
 708                  // intval, turning failure into 0, so do same here.
 709                  return 0;
 710              }
 711          } else {
 712              return $this->width;
 713          }
 714      }
 715  
 716      /**
 717       * Return the height of the image
 718       *
 719       * @param int $page
 720       * @return int
 721       */
 722  	public function getHeight( $page = 1 ) {
 723          $this->load();
 724  
 725          if ( $this->isMultipage() ) {
 726              $handler = $this->getHandler();
 727              if ( !$handler ) {
 728                  return 0;
 729              }
 730              $dim = $handler->getPageDimensions( $this, $page );
 731              if ( $dim ) {
 732                  return $dim['height'];
 733              } else {
 734                  // For non-paged media, the false goes through an
 735                  // intval, turning failure into 0, so do same here.
 736                  return 0;
 737              }
 738          } else {
 739              return $this->height;
 740          }
 741      }
 742  
 743      /**
 744       * Returns ID or name of user who uploaded the file
 745       *
 746       * @param string $type 'text' or 'id'
 747       * @return int|string
 748       */
 749  	function getUser( $type = 'text' ) {
 750          $this->load();
 751  
 752          if ( $type == 'text' ) {
 753              return $this->user_text;
 754          } elseif ( $type == 'id' ) {
 755              return $this->user;
 756          }
 757      }
 758  
 759      /**
 760       * Get handler-specific metadata
 761       * @return string
 762       */
 763  	function getMetadata() {
 764          $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
 765          return $this->metadata;
 766      }
 767  
 768      /**
 769       * @return int
 770       */
 771  	function getBitDepth() {
 772          $this->load();
 773  
 774          return $this->bits;
 775      }
 776  
 777      /**
 778       * Returns the size of the image file, in bytes
 779       * @return int
 780       */
 781  	public function getSize() {
 782          $this->load();
 783  
 784          return $this->size;
 785      }
 786  
 787      /**
 788       * Returns the MIME type of the file.
 789       * @return string
 790       */
 791  	function getMimeType() {
 792          $this->load();
 793  
 794          return $this->mime;
 795      }
 796  
 797      /**
 798       * Returns the type of the media in the file.
 799       * Use the value returned by this function with the MEDIATYPE_xxx constants.
 800       * @return string
 801       */
 802  	function getMediaType() {
 803          $this->load();
 804  
 805          return $this->media_type;
 806      }
 807  
 808      /** canRender inherited */
 809      /** mustRender inherited */
 810      /** allowInlineDisplay inherited */
 811      /** isSafeFile inherited */
 812      /** isTrustedFile inherited */
 813  
 814      /**
 815       * Returns true if the file exists on disk.
 816       * @return bool Whether file exist on disk.
 817       */
 818  	public function exists() {
 819          $this->load();
 820  
 821          return $this->fileExists;
 822      }
 823  
 824      /** getTransformScript inherited */
 825      /** getUnscaledThumb inherited */
 826      /** thumbName inherited */
 827      /** createThumb inherited */
 828      /** transform inherited */
 829  
 830      /** getHandler inherited */
 831      /** iconThumb inherited */
 832      /** getLastError inherited */
 833  
 834      /**
 835       * Get all thumbnail names previously generated for this file
 836       * @param string|bool $archiveName Name of an archive file, default false
 837       * @return array First element is the base dir, then files in that base dir.
 838       */
 839  	function getThumbnails( $archiveName = false ) {
 840          if ( $archiveName ) {
 841              $dir = $this->getArchiveThumbPath( $archiveName );
 842          } else {
 843              $dir = $this->getThumbPath();
 844          }
 845  
 846          $backend = $this->repo->getBackend();
 847          $files = array( $dir );
 848          try {
 849              $iterator = $backend->getFileList( array( 'dir' => $dir ) );
 850              foreach ( $iterator as $file ) {
 851                  $files[] = $file;
 852              }
 853          } catch ( FileBackendError $e ) {
 854          } // suppress (bug 54674)
 855  
 856          return $files;
 857      }
 858  
 859      /**
 860       * Refresh metadata in memcached, but don't touch thumbnails or squid
 861       */
 862  	function purgeMetadataCache() {
 863          $this->loadFromDB();
 864          $this->saveToCache();
 865          $this->purgeHistory();
 866      }
 867  
 868      /**
 869       * Purge the shared history (OldLocalFile) cache.
 870       *
 871       * @note This used to purge old thumbnails as well.
 872       */
 873  	function purgeHistory() {
 874          global $wgMemc;
 875  
 876          $hashedName = md5( $this->getName() );
 877          $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
 878  
 879          if ( $oldKey ) {
 880              $wgMemc->delete( $oldKey );
 881          }
 882      }
 883  
 884      /**
 885       * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid.
 886       *
 887       * @param array $options An array potentially with the key forThumbRefresh.
 888       *
 889       * @note This used to purge old thumbnails by default as well, but doesn't anymore.
 890       */
 891  	function purgeCache( $options = array() ) {
 892          wfProfileIn( __METHOD__ );
 893          // Refresh metadata cache
 894          $this->purgeMetadataCache();
 895  
 896          // Delete thumbnails
 897          $this->purgeThumbnails( $options );
 898  
 899          // Purge squid cache for this file
 900          SquidUpdate::purge( array( $this->getURL() ) );
 901          wfProfileOut( __METHOD__ );
 902      }
 903  
 904      /**
 905       * Delete cached transformed files for an archived version only.
 906       * @param string $archiveName Name of the archived file
 907       */
 908  	function purgeOldThumbnails( $archiveName ) {
 909          global $wgUseSquid;
 910          wfProfileIn( __METHOD__ );
 911  
 912          // Get a list of old thumbnails and URLs
 913          $files = $this->getThumbnails( $archiveName );
 914  
 915          // Purge any custom thumbnail caches
 916          wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
 917  
 918          $dir = array_shift( $files );
 919          $this->purgeThumbList( $dir, $files );
 920  
 921          // Purge the squid
 922          if ( $wgUseSquid ) {
 923              $urls = array();
 924              foreach ( $files as $file ) {
 925                  $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
 926              }
 927              SquidUpdate::purge( $urls );
 928          }
 929  
 930          wfProfileOut( __METHOD__ );
 931      }
 932  
 933      /**
 934       * Delete cached transformed files for the current version only.
 935       * @param array $options
 936       */
 937  	function purgeThumbnails( $options = array() ) {
 938          global $wgUseSquid;
 939          wfProfileIn( __METHOD__ );
 940  
 941          // Delete thumbnails
 942          $files = $this->getThumbnails();
 943          // Always purge all files from squid regardless of handler filters
 944          $urls = array();
 945          if ( $wgUseSquid ) {
 946              foreach ( $files as $file ) {
 947                  $urls[] = $this->getThumbUrl( $file );
 948              }
 949              array_shift( $urls ); // don't purge directory
 950          }
 951  
 952          // Give media handler a chance to filter the file purge list
 953          if ( !empty( $options['forThumbRefresh'] ) ) {
 954              $handler = $this->getHandler();
 955              if ( $handler ) {
 956                  $handler->filterThumbnailPurgeList( $files, $options );
 957              }
 958          }
 959  
 960          // Purge any custom thumbnail caches
 961          wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
 962  
 963          $dir = array_shift( $files );
 964          $this->purgeThumbList( $dir, $files );
 965  
 966          // Purge the squid
 967          if ( $wgUseSquid ) {
 968              SquidUpdate::purge( $urls );
 969          }
 970  
 971          wfProfileOut( __METHOD__ );
 972      }
 973  
 974      /**
 975       * Delete a list of thumbnails visible at urls
 976       * @param string $dir Base dir of the files.
 977       * @param array $files Array of strings: relative filenames (to $dir)
 978       */
 979  	protected function purgeThumbList( $dir, $files ) {
 980          $fileListDebug = strtr(
 981              var_export( $files, true ),
 982              array( "\n" => '' )
 983          );
 984          wfDebug( __METHOD__ . ": $fileListDebug\n" );
 985  
 986          $purgeList = array();
 987          foreach ( $files as $file ) {
 988              # Check that the base file name is part of the thumb name
 989              # This is a basic sanity check to avoid erasing unrelated directories
 990              if ( strpos( $file, $this->getName() ) !== false
 991                  || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
 992              ) {
 993                  $purgeList[] = "{$dir}/{$file}";
 994              }
 995          }
 996  
 997          # Delete the thumbnails
 998          $this->repo->quickPurgeBatch( $purgeList );
 999          # Clear out the thumbnail directory if empty
1000          $this->repo->quickCleanDir( $dir );
1001      }
1002  
1003      /** purgeDescription inherited */
1004      /** purgeEverything inherited */
1005  
1006      /**
1007       * @param int $limit Optional: Limit to number of results
1008       * @param int $start Optional: Timestamp, start from
1009       * @param int $end Optional: Timestamp, end at
1010       * @param bool $inc
1011       * @return array
1012       */
1013  	function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1014          $dbr = $this->repo->getSlaveDB();
1015          $tables = array( 'oldimage' );
1016          $fields = OldLocalFile::selectFields();
1017          $conds = $opts = $join_conds = array();
1018          $eq = $inc ? '=' : '';
1019          $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1020  
1021          if ( $start ) {
1022              $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1023          }
1024  
1025          if ( $end ) {
1026              $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1027          }
1028  
1029          if ( $limit ) {
1030              $opts['LIMIT'] = $limit;
1031          }
1032  
1033          // Search backwards for time > x queries
1034          $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1035          $opts['ORDER BY'] = "oi_timestamp $order";
1036          $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
1037  
1038          wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
1039              &$conds, &$opts, &$join_conds ) );
1040  
1041          $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1042          $r = array();
1043  
1044          foreach ( $res as $row ) {
1045              $r[] = $this->repo->newFileFromRow( $row );
1046          }
1047  
1048          if ( $order == 'ASC' ) {
1049              $r = array_reverse( $r ); // make sure it ends up descending
1050          }
1051  
1052          return $r;
1053      }
1054  
1055      /**
1056       * Returns the history of this file, line by line.
1057       * starts with current version, then old versions.
1058       * uses $this->historyLine to check which line to return:
1059       *  0      return line for current version
1060       *  1      query for old versions, return first one
1061       *  2, ... return next old version from above query
1062       * @return bool
1063       */
1064  	public function nextHistoryLine() {
1065          # Polymorphic function name to distinguish foreign and local fetches
1066          $fname = get_class( $this ) . '::' . __FUNCTION__;
1067  
1068          $dbr = $this->repo->getSlaveDB();
1069  
1070          if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1071              $this->historyRes = $dbr->select( 'image',
1072                  array(
1073                      '*',
1074                      "'' AS oi_archive_name",
1075                      '0 as oi_deleted',
1076                      'img_sha1'
1077                  ),
1078                  array( 'img_name' => $this->title->getDBkey() ),
1079                  $fname
1080              );
1081  
1082              if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1083                  $this->historyRes = null;
1084  
1085                  return false;
1086              }
1087          } elseif ( $this->historyLine == 1 ) {
1088              $this->historyRes = $dbr->select( 'oldimage', '*',
1089                  array( 'oi_name' => $this->title->getDBkey() ),
1090                  $fname,
1091                  array( 'ORDER BY' => 'oi_timestamp DESC' )
1092              );
1093          }
1094          $this->historyLine++;
1095  
1096          return $dbr->fetchObject( $this->historyRes );
1097      }
1098  
1099      /**
1100       * Reset the history pointer to the first element of the history
1101       */
1102  	public function resetHistory() {
1103          $this->historyLine = 0;
1104  
1105          if ( !is_null( $this->historyRes ) ) {
1106              $this->historyRes = null;
1107          }
1108      }
1109  
1110      /** getHashPath inherited */
1111      /** getRel inherited */
1112      /** getUrlRel inherited */
1113      /** getArchiveRel inherited */
1114      /** getArchivePath inherited */
1115      /** getThumbPath inherited */
1116      /** getArchiveUrl inherited */
1117      /** getThumbUrl inherited */
1118      /** getArchiveVirtualUrl inherited */
1119      /** getThumbVirtualUrl inherited */
1120      /** isHashed inherited */
1121  
1122      /**
1123       * Upload a file and record it in the DB
1124       * @param string $srcPath Source storage path, virtual URL, or filesystem path
1125       * @param string $comment Upload description
1126       * @param string $pageText Text to use for the new description page,
1127       *   if a new description page is created
1128       * @param int|bool $flags Flags for publish()
1129       * @param array|bool $props File properties, if known. This can be used to
1130       *   reduce the upload time when uploading virtual URLs for which the file
1131       *   info is already known
1132       * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
1133       *   current time
1134       * @param User|null $user User object or null to use $wgUser
1135       *
1136       * @return FileRepoStatus On success, the value member contains the
1137       *     archive name, or an empty string if it was a new file.
1138       */
1139  	function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false,
1140          $timestamp = false, $user = null
1141      ) {
1142          global $wgContLang;
1143  
1144          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1145              return $this->readOnlyFatalStatus();
1146          }
1147  
1148          if ( !$props ) {
1149              wfProfileIn( __METHOD__ . '-getProps' );
1150              if ( $this->repo->isVirtualUrl( $srcPath )
1151                  || FileBackend::isStoragePath( $srcPath )
1152              ) {
1153                  $props = $this->repo->getFileProps( $srcPath );
1154              } else {
1155                  $props = FSFile::getPropsFromPath( $srcPath );
1156              }
1157              wfProfileOut( __METHOD__ . '-getProps' );
1158          }
1159  
1160          $options = array();
1161          $handler = MediaHandler::getHandler( $props['mime'] );
1162          if ( $handler ) {
1163              $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1164          } else {
1165              $options['headers'] = array();
1166          }
1167  
1168          // Trim spaces on user supplied text
1169          $comment = trim( $comment );
1170  
1171          // Truncate nicely or the DB will do it for us
1172          // non-nicely (dangling multi-byte chars, non-truncated version in cache).
1173          $comment = $wgContLang->truncate( $comment, 255 );
1174          $this->lock(); // begin
1175          $status = $this->publish( $srcPath, $flags, $options );
1176  
1177          if ( $status->successCount >= 2 ) {
1178              // There will be a copy+(one of move,copy,store).
1179              // The first succeeding does not commit us to updating the DB
1180              // since it simply copied the current version to a timestamped file name.
1181              // It is only *preferable* to avoid leaving such files orphaned.
1182              // Once the second operation goes through, then the current version was
1183              // updated and we must therefore update the DB too.
1184              if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
1185                  $status->fatal( 'filenotfound', $srcPath );
1186              }
1187          }
1188  
1189          $this->unlock(); // done
1190  
1191          return $status;
1192      }
1193  
1194      /**
1195       * Record a file upload in the upload log and the image table
1196       * @param string $oldver
1197       * @param string $desc
1198       * @param string $license
1199       * @param string $copyStatus
1200       * @param string $source
1201       * @param bool $watch
1202       * @param string|bool $timestamp
1203       * @param User|null $user User object or null to use $wgUser
1204       * @return bool
1205       */
1206  	function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1207          $watch = false, $timestamp = false, User $user = null ) {
1208          if ( !$user ) {
1209              global $wgUser;
1210              $user = $wgUser;
1211          }
1212  
1213          $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1214  
1215          if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1216              return false;
1217          }
1218  
1219          if ( $watch ) {
1220              $user->addWatch( $this->getTitle() );
1221          }
1222  
1223          return true;
1224      }
1225  
1226      /**
1227       * Record a file upload in the upload log and the image table
1228       * @param string $oldver
1229       * @param string $comment
1230       * @param string $pageText
1231       * @param bool|array $props
1232       * @param string|bool $timestamp
1233       * @param null|User $user
1234       * @return bool
1235       */
1236  	function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false,
1237          $user = null
1238      ) {
1239          wfProfileIn( __METHOD__ );
1240  
1241          if ( is_null( $user ) ) {
1242              global $wgUser;
1243              $user = $wgUser;
1244          }
1245  
1246          $dbw = $this->repo->getMasterDB();
1247          $dbw->begin( __METHOD__ );
1248  
1249          if ( !$props ) {
1250              wfProfileIn( __METHOD__ . '-getProps' );
1251              $props = $this->repo->getFileProps( $this->getVirtualUrl() );
1252              wfProfileOut( __METHOD__ . '-getProps' );
1253          }
1254  
1255          # Imports or such might force a certain timestamp; otherwise we generate
1256          # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1257          if ( $timestamp === false ) {
1258              $timestamp = $dbw->timestamp();
1259              $allowTimeKludge = true;
1260          } else {
1261              $allowTimeKludge = false;
1262          }
1263  
1264          $props['description'] = $comment;
1265          $props['user'] = $user->getId();
1266          $props['user_text'] = $user->getName();
1267          $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1268          $this->setProps( $props );
1269  
1270          # Fail now if the file isn't there
1271          if ( !$this->fileExists ) {
1272              wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1273              $dbw->rollback( __METHOD__ );
1274              wfProfileOut( __METHOD__ );
1275  
1276              return false;
1277          }
1278  
1279          $reupload = false;
1280  
1281          # Test to see if the row exists using INSERT IGNORE
1282          # This avoids race conditions by locking the row until the commit, and also
1283          # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1284          $dbw->insert( 'image',
1285              array(
1286                  'img_name' => $this->getName(),
1287                  'img_size' => $this->size,
1288                  'img_width' => intval( $this->width ),
1289                  'img_height' => intval( $this->height ),
1290                  'img_bits' => $this->bits,
1291                  'img_media_type' => $this->media_type,
1292                  'img_major_mime' => $this->major_mime,
1293                  'img_minor_mime' => $this->minor_mime,
1294                  'img_timestamp' => $timestamp,
1295                  'img_description' => $comment,
1296                  'img_user' => $user->getId(),
1297                  'img_user_text' => $user->getName(),
1298                  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1299                  'img_sha1' => $this->sha1
1300              ),
1301              __METHOD__,
1302              'IGNORE'
1303          );
1304          if ( $dbw->affectedRows() == 0 ) {
1305              if ( $allowTimeKludge ) {
1306                  # Use FOR UPDATE to ignore any transaction snapshotting
1307                  $ltimestamp = $dbw->selectField( 'image', 'img_timestamp',
1308                      array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
1309                  $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1310                  # Avoid a timestamp that is not newer than the last version
1311                  # TODO: the image/oldimage tables should be like page/revision with an ID field
1312                  if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1313                      sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1314                      $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1315                      $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1316                  }
1317              }
1318  
1319              # (bug 34993) Note: $oldver can be empty here, if the previous
1320              # version of the file was broken. Allow registration of the new
1321              # version to continue anyway, because that's better than having
1322              # an image that's not fixable by user operations.
1323  
1324              $reupload = true;
1325              # Collision, this is an update of a file
1326              # Insert previous contents into oldimage
1327              $dbw->insertSelect( 'oldimage', 'image',
1328                  array(
1329                      'oi_name' => 'img_name',
1330                      'oi_archive_name' => $dbw->addQuotes( $oldver ),
1331                      'oi_size' => 'img_size',
1332                      'oi_width' => 'img_width',
1333                      'oi_height' => 'img_height',
1334                      'oi_bits' => 'img_bits',
1335                      'oi_timestamp' => 'img_timestamp',
1336                      'oi_description' => 'img_description',
1337                      'oi_user' => 'img_user',
1338                      'oi_user_text' => 'img_user_text',
1339                      'oi_metadata' => 'img_metadata',
1340                      'oi_media_type' => 'img_media_type',
1341                      'oi_major_mime' => 'img_major_mime',
1342                      'oi_minor_mime' => 'img_minor_mime',
1343                      'oi_sha1' => 'img_sha1'
1344                  ),
1345                  array( 'img_name' => $this->getName() ),
1346                  __METHOD__
1347              );
1348  
1349              # Update the current image row
1350              $dbw->update( 'image',
1351                  array( /* SET */
1352                      'img_size' => $this->size,
1353                      'img_width' => intval( $this->width ),
1354                      'img_height' => intval( $this->height ),
1355                      'img_bits' => $this->bits,
1356                      'img_media_type' => $this->media_type,
1357                      'img_major_mime' => $this->major_mime,
1358                      'img_minor_mime' => $this->minor_mime,
1359                      'img_timestamp' => $timestamp,
1360                      'img_description' => $comment,
1361                      'img_user' => $user->getId(),
1362                      'img_user_text' => $user->getName(),
1363                      'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1364                      'img_sha1' => $this->sha1
1365                  ),
1366                  array( 'img_name' => $this->getName() ),
1367                  __METHOD__
1368              );
1369          } else {
1370              # This is a new file, so update the image count
1371              DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
1372          }
1373  
1374          $descTitle = $this->getTitle();
1375          $wikiPage = new WikiFilePage( $descTitle );
1376          $wikiPage->setFile( $this );
1377  
1378          # Add the log entry
1379          $action = $reupload ? 'overwrite' : 'upload';
1380  
1381          $logEntry = new ManualLogEntry( 'upload', $action );
1382          $logEntry->setPerformer( $user );
1383          $logEntry->setComment( $comment );
1384          $logEntry->setTarget( $descTitle );
1385  
1386          // Allow people using the api to associate log entries with the upload.
1387          // Log has a timestamp, but sometimes different from upload timestamp.
1388          $logEntry->setParameters(
1389              array(
1390                  'img_sha1' => $this->sha1,
1391                  'img_timestamp' => $timestamp,
1392              )
1393          );
1394          // Note we keep $logId around since during new image
1395          // creation, page doesn't exist yet, so log_page = 0
1396          // but we want it to point to the page we're making,
1397          // so we later modify the log entry.
1398          // For a similar reason, we avoid making an RC entry
1399          // now and wait until the page exists.
1400          $logId = $logEntry->insert();
1401  
1402          $exists = $descTitle->exists();
1403          if ( $exists ) {
1404              // Page exists, do RC entry now (otherwise we wait for later).
1405              $logEntry->publish( $logId );
1406          }
1407          wfProfileIn( __METHOD__ . '-edit' );
1408  
1409          if ( $exists ) {
1410              # Create a null revision
1411              $latest = $descTitle->getLatestRevID();
1412              $editSummary = LogFormatter::newFromEntry( $logEntry )->getPlainActionText();
1413  
1414              $nullRevision = Revision::newNullRevision(
1415                  $dbw,
1416                  $descTitle->getArticleID(),
1417                  $editSummary,
1418                  false,
1419                  $user
1420              );
1421              if ( !is_null( $nullRevision ) ) {
1422                  $nullRevision->insertOn( $dbw );
1423  
1424                  wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
1425                  $wikiPage->updateRevisionOn( $dbw, $nullRevision );
1426              }
1427          }
1428  
1429          # Commit the transaction now, in case something goes wrong later
1430          # The most important thing is that files don't get lost, especially archives
1431          # NOTE: once we have support for nested transactions, the commit may be moved
1432          #       to after $wikiPage->doEdit has been called.
1433          $dbw->commit( __METHOD__ );
1434  
1435          # Save to memcache.
1436          # We shall not saveToCache before the commit since otherwise
1437          # in case of a rollback there is an usable file from memcached
1438          # which in fact doesn't really exist (bug 24978)
1439          $this->saveToCache();
1440  
1441          if ( $exists ) {
1442              # Invalidate the cache for the description page
1443              $descTitle->invalidateCache();
1444              $descTitle->purgeSquid();
1445          } else {
1446              # New file; create the description page.
1447              # There's already a log entry, so don't make a second RC entry
1448              # Squid and file cache for the description page are purged by doEditContent.
1449              $content = ContentHandler::makeContent( $pageText, $descTitle );
1450              $status = $wikiPage->doEditContent(
1451                  $content,
1452                  $comment,
1453                  EDIT_NEW | EDIT_SUPPRESS_RC,
1454                  false,
1455                  $user
1456              );
1457  
1458              $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
1459              // Now that the page exists, make an RC entry.
1460              $logEntry->publish( $logId );
1461              if ( isset( $status->value['revision'] ) ) {
1462                  $dbw->update( 'logging',
1463                      array( 'log_page' => $status->value['revision']->getPage() ),
1464                      array( 'log_id' => $logId ),
1465                      __METHOD__
1466                  );
1467              }
1468              $dbw->commit( __METHOD__ ); // commit before anything bad can happen
1469          }
1470  
1471          wfProfileOut( __METHOD__ . '-edit' );
1472  
1473          if ( $reupload ) {
1474              # Delete old thumbnails
1475              wfProfileIn( __METHOD__ . '-purge' );
1476              $this->purgeThumbnails();
1477              wfProfileOut( __METHOD__ . '-purge' );
1478  
1479              # Remove the old file from the squid cache
1480              SquidUpdate::purge( array( $this->getURL() ) );
1481          }
1482  
1483          # Hooks, hooks, the magic of hooks...
1484          wfProfileIn( __METHOD__ . '-hooks' );
1485          wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
1486          wfProfileOut( __METHOD__ . '-hooks' );
1487  
1488          # Invalidate cache for all pages using this file
1489          $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
1490          $update->doUpdate();
1491          if ( !$reupload ) {
1492              LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
1493          }
1494  
1495          wfProfileOut( __METHOD__ );
1496  
1497          return true;
1498      }
1499  
1500      /**
1501       * Move or copy a file to its public location. If a file exists at the
1502       * destination, move it to an archive. Returns a FileRepoStatus object with
1503       * the archive name in the "value" member on success.
1504       *
1505       * The archive name should be passed through to recordUpload for database
1506       * registration.
1507       *
1508       * @param string $srcPath Local filesystem path to the source image
1509       * @param int $flags A bitwise combination of:
1510       *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1511       * @param array $options Optional additional parameters
1512       * @return FileRepoStatus On success, the value member contains the
1513       *     archive name, or an empty string if it was a new file.
1514       */
1515  	function publish( $srcPath, $flags = 0, array $options = array() ) {
1516          return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
1517      }
1518  
1519      /**
1520       * Move or copy a file to a specified location. Returns a FileRepoStatus
1521       * object with the archive name in the "value" member on success.
1522       *
1523       * The archive name should be passed through to recordUpload for database
1524       * registration.
1525       *
1526       * @param string $srcPath Local filesystem path to the source image
1527       * @param string $dstRel Target relative path
1528       * @param int $flags A bitwise combination of:
1529       *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1530       * @param array $options Optional additional parameters
1531       * @return FileRepoStatus On success, the value member contains the
1532       *     archive name, or an empty string if it was a new file.
1533       */
1534  	function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
1535          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1536              return $this->readOnlyFatalStatus();
1537          }
1538  
1539          $this->lock(); // begin
1540  
1541          $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1542          $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1543          $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1544          $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1545  
1546          if ( $status->value == 'new' ) {
1547              $status->value = '';
1548          } else {
1549              $status->value = $archiveName;
1550          }
1551  
1552          $this->unlock(); // done
1553  
1554          return $status;
1555      }
1556  
1557      /** getLinksTo inherited */
1558      /** getExifData inherited */
1559      /** isLocal inherited */
1560      /** wasDeleted inherited */
1561  
1562      /**
1563       * Move file to the new title
1564       *
1565       * Move current, old version and all thumbnails
1566       * to the new filename. Old file is deleted.
1567       *
1568       * Cache purging is done; checks for validity
1569       * and logging are caller's responsibility
1570       *
1571       * @param Title $target New file name
1572       * @return FileRepoStatus
1573       */
1574  	function move( $target ) {
1575          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1576              return $this->readOnlyFatalStatus();
1577          }
1578  
1579          wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1580          $batch = new LocalFileMoveBatch( $this, $target );
1581  
1582          $this->lock(); // begin
1583          $batch->addCurrent();
1584          $archiveNames = $batch->addOlds();
1585          $status = $batch->execute();
1586          $this->unlock(); // done
1587  
1588          wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1589  
1590          // Purge the source and target files...
1591          $oldTitleFile = wfLocalFile( $this->title );
1592          $newTitleFile = wfLocalFile( $target );
1593          // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1594          // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1595          $this->getRepo()->getMasterDB()->onTransactionIdle(
1596              function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1597                  $oldTitleFile->purgeEverything();
1598                  foreach ( $archiveNames as $archiveName ) {
1599                      $oldTitleFile->purgeOldThumbnails( $archiveName );
1600                  }
1601                  $newTitleFile->purgeEverything();
1602              }
1603          );
1604  
1605          if ( $status->isOK() ) {
1606              // Now switch the object
1607              $this->title = $target;
1608              // Force regeneration of the name and hashpath
1609              unset( $this->name );
1610              unset( $this->hashPath );
1611          }
1612  
1613          return $status;
1614      }
1615  
1616      /**
1617       * Delete all versions of the file.
1618       *
1619       * Moves the files into an archive directory (or deletes them)
1620       * and removes the database rows.
1621       *
1622       * Cache purging is done; logging is caller's responsibility.
1623       *
1624       * @param string $reason
1625       * @param bool $suppress
1626       * @param User|null $user
1627       * @return FileRepoStatus
1628       */
1629  	function delete( $reason, $suppress = false, $user = null ) {
1630          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1631              return $this->readOnlyFatalStatus();
1632          }
1633  
1634          $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1635  
1636          $this->lock(); // begin
1637          $batch->addCurrent();
1638          # Get old version relative paths
1639          $archiveNames = $batch->addOlds();
1640          $status = $batch->execute();
1641          $this->unlock(); // done
1642  
1643          if ( $status->isOK() ) {
1644              DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
1645          }
1646  
1647          // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1648          // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1649          $file = $this;
1650          $this->getRepo()->getMasterDB()->onTransactionIdle(
1651              function () use ( $file, $archiveNames ) {
1652                  global $wgUseSquid;
1653  
1654                  $file->purgeEverything();
1655                  foreach ( $archiveNames as $archiveName ) {
1656                      $file->purgeOldThumbnails( $archiveName );
1657                  }
1658  
1659                  if ( $wgUseSquid ) {
1660                      // Purge the squid
1661                      $purgeUrls = array();
1662                      foreach ( $archiveNames as $archiveName ) {
1663                          $purgeUrls[] = $file->getArchiveUrl( $archiveName );
1664                      }
1665                      SquidUpdate::purge( $purgeUrls );
1666                  }
1667              }
1668          );
1669  
1670          return $status;
1671      }
1672  
1673      /**
1674       * Delete an old version of the file.
1675       *
1676       * Moves the file into an archive directory (or deletes it)
1677       * and removes the database row.
1678       *
1679       * Cache purging is done; logging is caller's responsibility.
1680       *
1681       * @param string $archiveName
1682       * @param string $reason
1683       * @param bool $suppress
1684       * @param User|null $user
1685       * @throws MWException Exception on database or file store failure
1686       * @return FileRepoStatus
1687       */
1688  	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1689          global $wgUseSquid;
1690          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1691              return $this->readOnlyFatalStatus();
1692          }
1693  
1694          $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1695  
1696          $this->lock(); // begin
1697          $batch->addOld( $archiveName );
1698          $status = $batch->execute();
1699          $this->unlock(); // done
1700  
1701          $this->purgeOldThumbnails( $archiveName );
1702          if ( $status->isOK() ) {
1703              $this->purgeDescription();
1704              $this->purgeHistory();
1705          }
1706  
1707          if ( $wgUseSquid ) {
1708              // Purge the squid
1709              SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
1710          }
1711  
1712          return $status;
1713      }
1714  
1715      /**
1716       * Restore all or specified deleted revisions to the given file.
1717       * Permissions and logging are left to the caller.
1718       *
1719       * May throw database exceptions on error.
1720       *
1721       * @param array $versions Set of record ids of deleted items to restore,
1722       *   or empty to restore all revisions.
1723       * @param bool $unsuppress
1724       * @return FileRepoStatus
1725       */
1726  	function restore( $versions = array(), $unsuppress = false ) {
1727          if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1728              return $this->readOnlyFatalStatus();
1729          }
1730  
1731          $batch = new LocalFileRestoreBatch( $this, $unsuppress );
1732  
1733          $this->lock(); // begin
1734          if ( !$versions ) {
1735              $batch->addAll();
1736          } else {
1737              $batch->addIds( $versions );
1738          }
1739          $status = $batch->execute();
1740          if ( $status->isGood() ) {
1741              $cleanupStatus = $batch->cleanup();
1742              $cleanupStatus->successCount = 0;
1743              $cleanupStatus->failCount = 0;
1744              $status->merge( $cleanupStatus );
1745          }
1746          $this->unlock(); // done
1747  
1748          return $status;
1749      }
1750  
1751      /** isMultipage inherited */
1752      /** pageCount inherited */
1753      /** scaleHeight inherited */
1754      /** getImageSize inherited */
1755  
1756      /**
1757       * Get the URL of the file description page.
1758       * @return string
1759       */
1760  	function getDescriptionUrl() {
1761          return $this->title->getLocalURL();
1762      }
1763  
1764      /**
1765       * Get the HTML text of the description page
1766       * This is not used by ImagePage for local files, since (among other things)
1767       * it skips the parser cache.
1768       *
1769       * @param Language $lang What language to get description in (Optional)
1770       * @return bool|mixed
1771       */
1772  	function getDescriptionText( $lang = null ) {
1773          $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1774          if ( !$revision ) {
1775              return false;
1776          }
1777          $content = $revision->getContent();
1778          if ( !$content ) {
1779              return false;
1780          }
1781          $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1782  
1783          return $pout->getText();
1784      }
1785  
1786      /**
1787       * @param int $audience
1788       * @param User $user
1789       * @return string
1790       */
1791  	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1792          $this->load();
1793          if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1794              return '';
1795          } elseif ( $audience == self::FOR_THIS_USER
1796              && !$this->userCan( self::DELETED_COMMENT, $user )
1797          ) {
1798              return '';
1799          } else {
1800              return $this->description;
1801          }
1802      }
1803  
1804      /**
1805       * @return bool|string
1806       */
1807  	function getTimestamp() {
1808          $this->load();
1809  
1810          return $this->timestamp;
1811      }
1812  
1813      /**
1814       * @return string
1815       */
1816  	function getSha1() {
1817          $this->load();
1818          // Initialise now if necessary
1819          if ( $this->sha1 == '' && $this->fileExists ) {
1820              $this->lock(); // begin
1821  
1822              $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1823              if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1824                  $dbw = $this->repo->getMasterDB();
1825                  $dbw->update( 'image',
1826                      array( 'img_sha1' => $this->sha1 ),
1827                      array( 'img_name' => $this->getName() ),
1828                      __METHOD__ );
1829                  $this->saveToCache();
1830              }
1831  
1832              $this->unlock(); // done
1833          }
1834  
1835          return $this->sha1;
1836      }
1837  
1838      /**
1839       * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1840       */
1841  	function isCacheable() {
1842          $this->load();
1843  
1844          // If extra data (metadata) was not loaded then it must have been large
1845          return $this->extraDataLoaded
1846          && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1847      }
1848  
1849      /**
1850       * Start a transaction and lock the image for update
1851       * Increments a reference counter if the lock is already held
1852       * @throws MWException Throws an error if the lock was not acquired
1853       * @return bool Success
1854       */
1855  	function lock() {
1856          $dbw = $this->repo->getMasterDB();
1857  
1858          if ( !$this->locked ) {
1859              if ( !$dbw->trxLevel() ) {
1860                  $dbw->begin( __METHOD__ );
1861                  $this->lockedOwnTrx = true;
1862              }
1863              $this->locked++;
1864              // Bug 54736: use simple lock to handle when the file does not exist.
1865              // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1866              // Also, that would cause contention on INSERT of similarly named rows.
1867              $backend = $this->getRepo()->getBackend();
1868              $lockPaths = array( $this->getPath() ); // represents all versions of the file
1869              $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
1870              if ( !$status->isGood() ) {
1871                  throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
1872              }
1873              $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
1874                  $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX ); // release on commit
1875              } );
1876          }
1877  
1878          $this->markVolatile(); // file may change soon
1879  
1880          return true;
1881      }
1882  
1883      /**
1884       * Decrement the lock reference count. If the reference count is reduced to zero, commits
1885       * the transaction and thereby releases the image lock.
1886       */
1887  	function unlock() {
1888          if ( $this->locked ) {
1889              --$this->locked;
1890              if ( !$this->locked && $this->lockedOwnTrx ) {
1891                  $dbw = $this->repo->getMasterDB();
1892                  $dbw->commit( __METHOD__ );
1893                  $this->lockedOwnTrx = false;
1894              }
1895          }
1896      }
1897  
1898      /**
1899       * Mark a file as about to be changed
1900       *
1901       * This sets a cache key that alters master/slave DB loading behavior
1902       *
1903       * @return bool Success
1904       */
1905  	protected function markVolatile() {
1906          global $wgMemc;
1907  
1908          $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) );
1909          if ( $key ) {
1910              $this->lastMarkedVolatile = time();
1911              return $wgMemc->set( $key, $this->lastMarkedVolatile, self::VOLATILE_TTL );
1912          }
1913  
1914          return true;
1915      }
1916  
1917      /**
1918       * Check if a file is about to be changed or has been changed recently
1919       *
1920       * @see LocalFile::isVolatile()
1921       * @return bool Whether the file is volatile
1922       */
1923  	protected function isVolatile() {
1924          global $wgMemc;
1925  
1926          $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) );
1927          if ( !$key ) {
1928              // repo unavailable; bail.
1929              return false;
1930          }
1931  
1932          if ( $this->lastMarkedVolatile === 0 ) {
1933              $this->lastMarkedVolatile = $wgMemc->get( $key ) ?: 0;
1934          }
1935  
1936          $volatileDuration = time() - $this->lastMarkedVolatile;
1937          return $volatileDuration <= self::VOLATILE_TTL;
1938      }
1939  
1940      /**
1941       * Roll back the DB transaction and mark the image unlocked
1942       */
1943  	function unlockAndRollback() {
1944          $this->locked = false;
1945          $dbw = $this->repo->getMasterDB();
1946          $dbw->rollback( __METHOD__ );
1947          $this->lockedOwnTrx = false;
1948      }
1949  
1950      /**
1951       * @return Status
1952       */
1953  	protected function readOnlyFatalStatus() {
1954          return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
1955              $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
1956      }
1957  
1958      /**
1959       * Clean up any dangling locks
1960       */
1961  	function __destruct() {
1962          $this->unlock();
1963      }
1964  } // LocalFile class
1965  
1966  # ------------------------------------------------------------------------------
1967  
1968  /**
1969   * Helper class for file deletion
1970   * @ingroup FileAbstraction
1971   */
1972  class LocalFileDeleteBatch {
1973      /** @var LocalFile */
1974      private $file;
1975  
1976      /** @var string */
1977      private $reason;
1978  
1979      /** @var array */
1980      private $srcRels = array();
1981  
1982      /** @var array */
1983      private $archiveUrls = array();
1984  
1985      /** @var array Items to be processed in the deletion batch */
1986      private $deletionBatch;
1987  
1988      /** @var bool Wether to suppress all suppressable fields when deleting */
1989      private $suppress;
1990  
1991      /** @var FileRepoStatus */
1992      private $status;
1993  
1994      /** @var User */
1995      private $user;
1996  
1997      /**
1998       * @param File $file
1999       * @param string $reason
2000       * @param bool $suppress
2001       * @param User|null $user
2002       */
2003  	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2004          $this->file = $file;
2005          $this->reason = $reason;
2006          $this->suppress = $suppress;
2007          if ( $user ) {
2008              $this->user = $user;
2009          } else {
2010              global $wgUser;
2011              $this->user = $wgUser;
2012          }
2013          $this->status = $file->repo->newGood();
2014      }
2015  
2016  	function addCurrent() {
2017          $this->srcRels['.'] = $this->file->getRel();
2018      }
2019  
2020      /**
2021       * @param string $oldName
2022       */
2023  	function addOld( $oldName ) {
2024          $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2025          $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2026      }
2027  
2028      /**
2029       * Add the old versions of the image to the batch
2030       * @return array List of archive names from old versions
2031       */
2032  	function addOlds() {
2033          $archiveNames = array();
2034  
2035          $dbw = $this->file->repo->getMasterDB();
2036          $result = $dbw->select( 'oldimage',
2037              array( 'oi_archive_name' ),
2038              array( 'oi_name' => $this->file->getName() ),
2039              __METHOD__
2040          );
2041  
2042          foreach ( $result as $row ) {
2043              $this->addOld( $row->oi_archive_name );
2044              $archiveNames[] = $row->oi_archive_name;
2045          }
2046  
2047          return $archiveNames;
2048      }
2049  
2050      /**
2051       * @return array
2052       */
2053  	function getOldRels() {
2054          if ( !isset( $this->srcRels['.'] ) ) {
2055              $oldRels =& $this->srcRels;
2056              $deleteCurrent = false;
2057          } else {
2058              $oldRels = $this->srcRels;
2059              unset( $oldRels['.'] );
2060              $deleteCurrent = true;
2061          }
2062  
2063          return array( $oldRels, $deleteCurrent );
2064      }
2065  
2066      /**
2067       * @return array
2068       */
2069  	protected function getHashes() {
2070          $hashes = array();
2071          list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2072  
2073          if ( $deleteCurrent ) {
2074              $hashes['.'] = $this->file->getSha1();
2075          }
2076  
2077          if ( count( $oldRels ) ) {
2078              $dbw = $this->file->repo->getMasterDB();
2079              $res = $dbw->select(
2080                  'oldimage',
2081                  array( 'oi_archive_name', 'oi_sha1' ),
2082                  array( 'oi_archive_name' => array_keys( $oldRels ),
2083                      'oi_name' => $this->file->getName() ), // performance
2084                  __METHOD__
2085              );
2086  
2087              foreach ( $res as $row ) {
2088                  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2089                      // Get the hash from the file
2090                      $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2091                      $props = $this->file->repo->getFileProps( $oldUrl );
2092  
2093                      if ( $props['fileExists'] ) {
2094                          // Upgrade the oldimage row
2095                          $dbw->update( 'oldimage',
2096                              array( 'oi_sha1' => $props['sha1'] ),
2097                              array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
2098                              __METHOD__ );
2099                          $hashes[$row->oi_archive_name] = $props['sha1'];
2100                      } else {
2101                          $hashes[$row->oi_archive_name] = false;
2102                      }
2103                  } else {
2104                      $hashes[$row->oi_archive_name] = $row->oi_sha1;
2105                  }
2106              }
2107          }
2108  
2109          $missing = array_diff_key( $this->srcRels, $hashes );
2110  
2111          foreach ( $missing as $name => $rel ) {
2112              $this->status->error( 'filedelete-old-unregistered', $name );
2113          }
2114  
2115          foreach ( $hashes as $name => $hash ) {
2116              if ( !$hash ) {
2117                  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2118                  unset( $hashes[$name] );
2119              }
2120          }
2121  
2122          return $hashes;
2123      }
2124  
2125  	function doDBInserts() {
2126          $dbw = $this->file->repo->getMasterDB();
2127          $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2128          $encUserId = $dbw->addQuotes( $this->user->getId() );
2129          $encReason = $dbw->addQuotes( $this->reason );
2130          $encGroup = $dbw->addQuotes( 'deleted' );
2131          $ext = $this->file->getExtension();
2132          $dotExt = $ext === '' ? '' : ".$ext";
2133          $encExt = $dbw->addQuotes( $dotExt );
2134          list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2135  
2136          // Bitfields to further suppress the content
2137          if ( $this->suppress ) {
2138              $bitfield = 0;
2139              // This should be 15...
2140              $bitfield |= Revision::DELETED_TEXT;
2141              $bitfield |= Revision::DELETED_COMMENT;
2142              $bitfield |= Revision::DELETED_USER;
2143              $bitfield |= Revision::DELETED_RESTRICTED;
2144          } else {
2145              $bitfield = 'oi_deleted';
2146          }
2147  
2148          if ( $deleteCurrent ) {
2149              $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
2150              $where = array( 'img_name' => $this->file->getName() );
2151              $dbw->insertSelect( 'filearchive', 'image',
2152                  array(
2153                      'fa_storage_group' => $encGroup,
2154                      'fa_storage_key' => $dbw->conditional(
2155                          array( 'img_sha1' => '' ),
2156                          $dbw->addQuotes( '' ),
2157                          $concat
2158                      ),
2159                      'fa_deleted_user' => $encUserId,
2160                      'fa_deleted_timestamp' => $encTimestamp,
2161                      'fa_deleted_reason' => $encReason,
2162                      'fa_deleted' => $this->suppress ? $bitfield : 0,
2163  
2164                      'fa_name' => 'img_name',
2165                      'fa_archive_name' => 'NULL',
2166                      'fa_size' => 'img_size',
2167                      'fa_width' => 'img_width',
2168                      'fa_height' => 'img_height',
2169                      'fa_metadata' => 'img_metadata',
2170                      'fa_bits' => 'img_bits',
2171                      'fa_media_type' => 'img_media_type',
2172                      'fa_major_mime' => 'img_major_mime',
2173                      'fa_minor_mime' => 'img_minor_mime',
2174                      'fa_description' => 'img_description',
2175                      'fa_user' => 'img_user',
2176                      'fa_user_text' => 'img_user_text',
2177                      'fa_timestamp' => 'img_timestamp',
2178                      'fa_sha1' => 'img_sha1',
2179                  ), $where, __METHOD__ );
2180          }
2181  
2182          if ( count( $oldRels ) ) {
2183              $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
2184              $where = array(
2185                  'oi_name' => $this->file->getName(),
2186                  'oi_archive_name' => array_keys( $oldRels ) );
2187              $dbw->insertSelect( 'filearchive', 'oldimage',
2188                  array(
2189                      'fa_storage_group' => $encGroup,
2190                      'fa_storage_key' => $dbw->conditional(
2191                          array( 'oi_sha1' => '' ),
2192                          $dbw->addQuotes( '' ),
2193                          $concat
2194                      ),
2195                      'fa_deleted_user' => $encUserId,
2196                      'fa_deleted_timestamp' => $encTimestamp,
2197                      'fa_deleted_reason' => $encReason,
2198                      'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2199  
2200                      'fa_name' => 'oi_name',
2201                      'fa_archive_name' => 'oi_archive_name',
2202                      'fa_size' => 'oi_size',
2203                      'fa_width' => 'oi_width',
2204                      'fa_height' => 'oi_height',
2205                      'fa_metadata' => 'oi_metadata',
2206                      'fa_bits' => 'oi_bits',
2207                      'fa_media_type' => 'oi_media_type',
2208                      'fa_major_mime' => 'oi_major_mime',
2209                      'fa_minor_mime' => 'oi_minor_mime',
2210                      'fa_description' => 'oi_description',
2211                      'fa_user' => 'oi_user',
2212                      'fa_user_text' => 'oi_user_text',
2213                      'fa_timestamp' => 'oi_timestamp',
2214                      'fa_sha1' => 'oi_sha1',
2215                  ), $where, __METHOD__ );
2216          }
2217      }
2218  
2219  	function doDBDeletes() {
2220          $dbw = $this->file->repo->getMasterDB();
2221          list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2222  
2223          if ( count( $oldRels ) ) {
2224              $dbw->delete( 'oldimage',
2225                  array(
2226                      'oi_name' => $this->file->getName(),
2227                      'oi_archive_name' => array_keys( $oldRels )
2228                  ), __METHOD__ );
2229          }
2230  
2231          if ( $deleteCurrent ) {
2232              $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
2233          }
2234      }
2235  
2236      /**
2237       * Run the transaction
2238       * @return FileRepoStatus
2239       */
2240  	function execute() {
2241          wfProfileIn( __METHOD__ );
2242  
2243          $this->file->lock();
2244          // Leave private files alone
2245          $privateFiles = array();
2246          list( $oldRels, ) = $this->getOldRels();
2247          $dbw = $this->file->repo->getMasterDB();
2248  
2249          if ( !empty( $oldRels ) ) {
2250              $res = $dbw->select( 'oldimage',
2251                  array( 'oi_archive_name' ),
2252                  array( 'oi_name' => $this->file->getName(),
2253                      'oi_archive_name' => array_keys( $oldRels ),
2254                      $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
2255                  __METHOD__ );
2256  
2257              foreach ( $res as $row ) {
2258                  $privateFiles[$row->oi_archive_name] = 1;
2259              }
2260          }
2261          // Prepare deletion batch
2262          $hashes = $this->getHashes();
2263          $this->deletionBatch = array();
2264          $ext = $this->file->getExtension();
2265          $dotExt = $ext === '' ? '' : ".$ext";
2266  
2267          foreach ( $this->srcRels as $name => $srcRel ) {
2268              // Skip files that have no hash (missing source).
2269              // Keep private files where they are.
2270              if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
2271                  $hash = $hashes[$name];
2272                  $key = $hash . $dotExt;
2273                  $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
2274                  $this->deletionBatch[$name] = array( $srcRel, $dstRel );
2275              }
2276          }
2277  
2278          // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
2279          // We acquire this lock by running the inserts now, before the file operations.
2280          //
2281          // This potentially has poor lock contention characteristics -- an alternative
2282          // scheme would be to insert stub filearchive entries with no fa_name and commit
2283          // them in a separate transaction, then run the file ops, then update the fa_name fields.
2284          $this->doDBInserts();
2285  
2286          // Removes non-existent file from the batch, so we don't get errors.
2287          $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2288          if ( !$checkStatus->isGood() ) {
2289              $this->status->merge( $checkStatus );
2290              return $this->status;
2291          }
2292          $this->deletionBatch = $checkStatus->value;
2293  
2294          // Execute the file deletion batch
2295          $status = $this->file->repo->deleteBatch( $this->deletionBatch );
2296  
2297          if ( !$status->isGood() ) {
2298              $this->status->merge( $status );
2299          }
2300  
2301          if ( !$this->status->isOK() ) {
2302              // Critical file deletion error
2303              // Roll back inserts, release lock and abort
2304              // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
2305              $this->file->unlockAndRollback();
2306              wfProfileOut( __METHOD__ );
2307  
2308              return $this->status;
2309          }
2310  
2311          // Delete image/oldimage rows
2312          $this->doDBDeletes();
2313  
2314          // Commit and return
2315          $this->file->unlock();
2316          wfProfileOut( __METHOD__ );
2317  
2318          return $this->status;
2319      }
2320  
2321      /**
2322       * Removes non-existent files from a deletion batch.
2323       * @param array $batch
2324       * @return Status
2325       */
2326  	function removeNonexistentFiles( $batch ) {
2327          $files = $newBatch = array();
2328  
2329          foreach ( $batch as $batchItem ) {
2330              list( $src, ) = $batchItem;
2331              $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2332          }
2333  
2334          $result = $this->file->repo->fileExistsBatch( $files );
2335          if ( in_array( null, $result, true ) ) {
2336              return Status::newFatal( 'backend-fail-internal',
2337                  $this->file->repo->getBackend()->getName() );
2338          }
2339  
2340          foreach ( $batch as $batchItem ) {
2341              if ( $result[$batchItem[0]] ) {
2342                  $newBatch[] = $batchItem;
2343              }
2344          }
2345  
2346          return Status::newGood( $newBatch );
2347      }
2348  }
2349  
2350  # ------------------------------------------------------------------------------
2351  
2352  /**
2353   * Helper class for file undeletion
2354   * @ingroup FileAbstraction
2355   */
2356  class LocalFileRestoreBatch {
2357      /** @var LocalFile */
2358      private $file;
2359  
2360      /** @var array List of file IDs to restore */
2361      private $cleanupBatch;
2362  
2363      /** @var array List of file IDs to restore */
2364      private $ids;
2365  
2366      /** @var bool Add all revisions of the file */
2367      private $all;
2368  
2369      /** @var bool Wether to remove all settings for suppressed fields */
2370      private $unsuppress = false;
2371  
2372      /**
2373       * @param File $file
2374       * @param bool $unsuppress
2375       */
2376  	function __construct( File $file, $unsuppress = false ) {
2377          $this->file = $file;
2378          $this->cleanupBatch = $this->ids = array();
2379          $this->ids = array();
2380          $this->unsuppress = $unsuppress;
2381      }
2382  
2383      /**
2384       * Add a file by ID
2385       * @param int $fa_id
2386       */
2387  	function addId( $fa_id ) {
2388          $this->ids[] = $fa_id;
2389      }
2390  
2391      /**
2392       * Add a whole lot of files by ID
2393       * @param int[] $ids
2394       */
2395  	function addIds( $ids ) {
2396          $this->ids = array_merge( $this->ids, $ids );
2397      }
2398  
2399      /**
2400       * Add all revisions of the file
2401       */
2402  	function addAll() {
2403          $this->all = true;
2404      }
2405  
2406      /**
2407       * Run the transaction, except the cleanup batch.
2408       * The cleanup batch should be run in a separate transaction, because it locks different
2409       * rows and there's no need to keep the image row locked while it's acquiring those locks
2410       * The caller may have its own transaction open.
2411       * So we save the batch and let the caller call cleanup()
2412       * @return FileRepoStatus
2413       */
2414  	function execute() {
2415          global $wgLang;
2416  
2417          if ( !$this->all && !$this->ids ) {
2418              // Do nothing
2419              return $this->file->repo->newGood();
2420          }
2421  
2422          $this->file->lock();
2423  
2424          $dbw = $this->file->repo->getMasterDB();
2425          $status = $this->file->repo->newGood();
2426  
2427          $exists = (bool)$dbw->selectField( 'image', '1',
2428              array( 'img_name' => $this->file->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
2429  
2430          // Fetch all or selected archived revisions for the file,
2431          // sorted from the most recent to the oldest.
2432          $conditions = array( 'fa_name' => $this->file->getName() );
2433  
2434          if ( !$this->all ) {
2435              $conditions['fa_id'] = $this->ids;
2436          }
2437  
2438          $result = $dbw->select(
2439              'filearchive',
2440              ArchivedFile::selectFields(),
2441              $conditions,
2442              __METHOD__,
2443              array( 'ORDER BY' => 'fa_timestamp DESC' )
2444          );
2445  
2446          $idsPresent = array();
2447          $storeBatch = array();
2448          $insertBatch = array();
2449          $insertCurrent = false;
2450          $deleteIds = array();
2451          $first = true;
2452          $archiveNames = array();
2453  
2454          foreach ( $result as $row ) {
2455              $idsPresent[] = $row->fa_id;
2456  
2457              if ( $row->fa_name != $this->file->getName() ) {
2458                  $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2459                  $status->failCount++;
2460                  continue;
2461              }
2462  
2463              if ( $row->fa_storage_key == '' ) {
2464                  // Revision was missing pre-deletion
2465                  $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2466                  $status->failCount++;
2467                  continue;
2468              }
2469  
2470              $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) .
2471                  $row->fa_storage_key;
2472              $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2473  
2474              if ( isset( $row->fa_sha1 ) ) {
2475                  $sha1 = $row->fa_sha1;
2476              } else {
2477                  // old row, populate from key
2478                  $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2479              }
2480  
2481              # Fix leading zero
2482              if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2483                  $sha1 = substr( $sha1, 1 );
2484              }
2485  
2486              if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2487                  || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2488                  || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2489                  || is_null( $row->fa_metadata )
2490              ) {
2491                  // Refresh our metadata
2492                  // Required for a new current revision; nice for older ones too. :)
2493                  $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2494              } else {
2495                  $props = array(
2496                      'minor_mime' => $row->fa_minor_mime,
2497                      'major_mime' => $row->fa_major_mime,
2498                      'media_type' => $row->fa_media_type,
2499                      'metadata' => $row->fa_metadata
2500                  );
2501              }
2502  
2503              if ( $first && !$exists ) {
2504                  // This revision will be published as the new current version
2505                  $destRel = $this->file->getRel();
2506                  $insertCurrent = array(
2507                      'img_name' => $row->fa_name,
2508                      'img_size' => $row->fa_size,
2509                      'img_width' => $row->fa_width,
2510                      'img_height' => $row->fa_height,
2511                      'img_metadata' => $props['metadata'],
2512                      'img_bits' => $row->fa_bits,
2513                      'img_media_type' => $props['media_type'],
2514                      'img_major_mime' => $props['major_mime'],
2515                      'img_minor_mime' => $props['minor_mime'],
2516                      'img_description' => $row->fa_description,
2517                      'img_user' => $row->fa_user,
2518                      'img_user_text' => $row->fa_user_text,
2519                      'img_timestamp' => $row->fa_timestamp,
2520                      'img_sha1' => $sha1
2521                  );
2522  
2523                  // The live (current) version cannot be hidden!
2524                  if ( !$this->unsuppress && $row->fa_deleted ) {
2525                      $storeBatch[] = array( $deletedUrl, 'public', $destRel );
2526                      $this->cleanupBatch[] = $row->fa_storage_key;
2527                  }
2528              } else {
2529                  $archiveName = $row->fa_archive_name;
2530  
2531                  if ( $archiveName == '' ) {
2532                      // This was originally a current version; we
2533                      // have to devise a new archive name for it.
2534                      // Format is <timestamp of archiving>!<name>
2535                      $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2536  
2537                      do {
2538                          $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2539                          $timestamp++;
2540                      } while ( isset( $archiveNames[$archiveName] ) );
2541                  }
2542  
2543                  $archiveNames[$archiveName] = true;
2544                  $destRel = $this->file->getArchiveRel( $archiveName );
2545                  $insertBatch[] = array(
2546                      'oi_name' => $row->fa_name,
2547                      'oi_archive_name' => $archiveName,
2548                      'oi_size' => $row->fa_size,
2549                      'oi_width' => $row->fa_width,
2550                      'oi_height' => $row->fa_height,
2551                      'oi_bits' => $row->fa_bits,
2552                      'oi_description' => $row->fa_description,
2553                      'oi_user' => $row->fa_user,
2554                      'oi_user_text' => $row->fa_user_text,
2555                      'oi_timestamp' => $row->fa_timestamp,
2556                      'oi_metadata' => $props['metadata'],
2557                      'oi_media_type' => $props['media_type'],
2558                      'oi_major_mime' => $props['major_mime'],
2559                      'oi_minor_mime' => $props['minor_mime'],
2560                      'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2561                      'oi_sha1' => $sha1 );
2562              }
2563  
2564              $deleteIds[] = $row->fa_id;
2565  
2566              if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2567                  // private files can stay where they are
2568                  $status->successCount++;
2569              } else {
2570                  $storeBatch[] = array( $deletedUrl, 'public', $destRel );
2571                  $this->cleanupBatch[] = $row->fa_storage_key;
2572              }
2573  
2574              $first = false;
2575          }
2576  
2577          unset( $result );
2578  
2579          // Add a warning to the status object for missing IDs
2580          $missingIds = array_diff( $this->ids, $idsPresent );
2581  
2582          foreach ( $missingIds as $id ) {
2583              $status->error( 'undelete-missing-filearchive', $id );
2584          }
2585  
2586          // Remove missing files from batch, so we don't get errors when undeleting them
2587          $checkStatus = $this->removeNonexistentFiles( $storeBatch );
2588          if ( !$checkStatus->isGood() ) {
2589              $status->merge( $checkStatus );
2590              return $status;
2591          }
2592          $storeBatch = $checkStatus->value;
2593  
2594          // Run the store batch
2595          // Use the OVERWRITE_SAME flag to smooth over a common error
2596          $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2597          $status->merge( $storeStatus );
2598  
2599          if ( !$status->isGood() ) {
2600              // Even if some files could be copied, fail entirely as that is the
2601              // easiest thing to do without data loss
2602              $this->cleanupFailedBatch( $storeStatus, $storeBatch );
2603              $status->ok = false;
2604              $this->file->unlock();
2605  
2606              return $status;
2607          }
2608  
2609          // Run the DB updates
2610          // Because we have locked the image row, key conflicts should be rare.
2611          // If they do occur, we can roll back the transaction at this time with
2612          // no data loss, but leaving unregistered files scattered throughout the
2613          // public zone.
2614          // This is not ideal, which is why it's important to lock the image row.
2615          if ( $insertCurrent ) {
2616              $dbw->insert( 'image', $insertCurrent, __METHOD__ );
2617          }
2618  
2619          if ( $insertBatch ) {
2620              $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2621          }
2622  
2623          if ( $deleteIds ) {
2624              $dbw->delete( 'filearchive',
2625                  array( 'fa_id' => $deleteIds ),
2626                  __METHOD__ );
2627          }
2628  
2629          // If store batch is empty (all files are missing), deletion is to be considered successful
2630          if ( $status->successCount > 0 || !$storeBatch ) {
2631              if ( !$exists ) {
2632                  wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2633  
2634                  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
2635  
2636                  $this->file->purgeEverything();
2637              } else {
2638                  wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2639                  $this->file->purgeDescription();
2640                  $this->file->purgeHistory();
2641              }
2642          }
2643  
2644          $this->file->unlock();
2645  
2646          return $status;
2647      }
2648  
2649      /**
2650       * Removes non-existent files from a store batch.
2651       * @param array $triplets
2652       * @return Status
2653       */
2654  	function removeNonexistentFiles( $triplets ) {
2655          $files = $filteredTriplets = array();
2656          foreach ( $triplets as $file ) {
2657              $files[$file[0]] = $file[0];
2658          }
2659  
2660          $result = $this->file->repo->fileExistsBatch( $files );
2661          if ( in_array( null, $result, true ) ) {
2662              return Status::newFatal( 'backend-fail-internal',
2663                  $this->file->repo->getBackend()->getName() );
2664          }
2665  
2666          foreach ( $triplets as $file ) {
2667              if ( $result[$file[0]] ) {
2668                  $filteredTriplets[] = $file;
2669              }
2670          }
2671  
2672          return Status::newGood( $filteredTriplets );
2673      }
2674  
2675      /**
2676       * Removes non-existent files from a cleanup batch.
2677       * @param array $batch
2678       * @return array
2679       */
2680  	function removeNonexistentFromCleanup( $batch ) {
2681          $files = $newBatch = array();
2682          $repo = $this->file->repo;
2683  
2684          foreach ( $batch as $file ) {
2685              $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2686                  rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2687          }
2688  
2689          $result = $repo->fileExistsBatch( $files );
2690  
2691          foreach ( $batch as $file ) {
2692              if ( $result[$file] ) {
2693                  $newBatch[] = $file;
2694              }
2695          }
2696  
2697          return $newBatch;
2698      }
2699  
2700      /**
2701       * Delete unused files in the deleted zone.
2702       * This should be called from outside the transaction in which execute() was called.
2703       * @return FileRepoStatus
2704       */
2705  	function cleanup() {
2706          if ( !$this->cleanupBatch ) {
2707              return $this->file->repo->newGood();
2708          }
2709  
2710          $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2711  
2712          $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2713  
2714          return $status;
2715      }
2716  
2717      /**
2718       * Cleanup a failed batch. The batch was only partially successful, so
2719       * rollback by removing all items that were succesfully copied.
2720       *
2721       * @param Status $storeStatus
2722       * @param array $storeBatch
2723       */
2724  	function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2725          $cleanupBatch = array();
2726  
2727          foreach ( $storeStatus->success as $i => $success ) {
2728              // Check if this item of the batch was successfully copied
2729              if ( $success ) {
2730                  // Item was successfully copied and needs to be removed again
2731                  // Extract ($dstZone, $dstRel) from the batch
2732                  $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
2733              }
2734          }
2735          $this->file->repo->cleanupBatch( $cleanupBatch );
2736      }
2737  }
2738  
2739  # ------------------------------------------------------------------------------
2740  
2741  /**
2742   * Helper class for file movement
2743   * @ingroup FileAbstraction
2744   */
2745  class LocalFileMoveBatch {
2746      /** @var LocalFile */
2747      protected $file;
2748  
2749      /** @var Title */
2750      protected $target;
2751  
2752      protected $cur;
2753  
2754      protected $olds;
2755  
2756      protected $oldCount;
2757  
2758      protected $archive;
2759  
2760      /** @var DatabaseBase */
2761      protected $db;
2762  
2763      /**
2764       * @param File $file
2765       * @param Title $target
2766       */
2767  	function __construct( File $file, Title $target ) {
2768          $this->file = $file;
2769          $this->target = $target;
2770          $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2771          $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2772          $this->oldName = $this->file->getName();
2773          $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2774          $this->oldRel = $this->oldHash . $this->oldName;
2775          $this->newRel = $this->newHash . $this->newName;
2776          $this->db = $file->getRepo()->getMasterDb();
2777      }
2778  
2779      /**
2780       * Add the current image to the batch
2781       */
2782  	function addCurrent() {
2783          $this->cur = array( $this->oldRel, $this->newRel );
2784      }
2785  
2786      /**
2787       * Add the old versions of the image to the batch
2788       * @return array List of archive names from old versions
2789       */
2790  	function addOlds() {
2791          $archiveBase = 'archive';
2792          $this->olds = array();
2793          $this->oldCount = 0;
2794          $archiveNames = array();
2795  
2796          $result = $this->db->select( 'oldimage',
2797              array( 'oi_archive_name', 'oi_deleted' ),
2798              array( 'oi_name' => $this->oldName ),
2799              __METHOD__,
2800              array( 'FOR UPDATE' ) // ignore snapshot
2801          );
2802  
2803          foreach ( $result as $row ) {
2804              $archiveNames[] = $row->oi_archive_name;
2805              $oldName = $row->oi_archive_name;
2806              $bits = explode( '!', $oldName, 2 );
2807  
2808              if ( count( $bits ) != 2 ) {
2809                  wfDebug( "Old file name missing !: '$oldName' \n" );
2810                  continue;
2811              }
2812  
2813              list( $timestamp, $filename ) = $bits;
2814  
2815              if ( $this->oldName != $filename ) {
2816                  wfDebug( "Old file name doesn't match: '$oldName' \n" );
2817                  continue;
2818              }
2819  
2820              $this->oldCount++;
2821  
2822              // Do we want to add those to oldCount?
2823              if ( $row->oi_deleted & File::DELETED_FILE ) {
2824                  continue;
2825              }
2826  
2827              $this->olds[] = array(
2828                  "{$archiveBase}/{$this->oldHash}{$oldName}",
2829                  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2830              );
2831          }
2832  
2833          return $archiveNames;
2834      }
2835  
2836      /**
2837       * Perform the move.
2838       * @return FileRepoStatus
2839       */
2840  	function execute() {
2841          $repo = $this->file->repo;
2842          $status = $repo->newGood();
2843  
2844          $triplets = $this->getMoveTriplets();
2845          $checkStatus = $this->removeNonexistentFiles( $triplets );
2846          if ( !$checkStatus->isGood() ) {
2847              $status->merge( $checkStatus );
2848              return $status;
2849          }
2850          $triplets = $checkStatus->value;
2851          $destFile = wfLocalFile( $this->target );
2852  
2853          $this->file->lock(); // begin
2854          $destFile->lock(); // quickly fail if destination is not available
2855          // Rename the file versions metadata in the DB.
2856          // This implicitly locks the destination file, which avoids race conditions.
2857          // If we moved the files from A -> C before DB updates, another process could
2858          // move files from B -> C at this point, causing storeBatch() to fail and thus
2859          // cleanupTarget() to trigger. It would delete the C files and cause data loss.
2860          $statusDb = $this->doDBUpdates();
2861          if ( !$statusDb->isGood() ) {
2862              $destFile->unlock();
2863              $this->file->unlockAndRollback();
2864              $statusDb->ok = false;
2865  
2866              return $statusDb;
2867          }
2868          wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2869              "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2870  
2871          // Copy the files into their new location.
2872          // If a prior process fataled copying or cleaning up files we tolerate any
2873          // of the existing files if they are identical to the ones being stored.
2874          $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2875          wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2876              "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2877          if ( !$statusMove->isGood() ) {
2878              // Delete any files copied over (while the destination is still locked)
2879              $this->cleanupTarget( $triplets );
2880              $destFile->unlock();
2881              $this->file->unlockAndRollback(); // unlocks the destination
2882              wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
2883              $statusMove->ok = false;
2884  
2885              return $statusMove;
2886          }
2887          $destFile->unlock();
2888          $this->file->unlock(); // done
2889  
2890          // Everything went ok, remove the source files
2891          $this->cleanupSource( $triplets );
2892  
2893          $status->merge( $statusDb );
2894          $status->merge( $statusMove );
2895  
2896          return $status;
2897      }
2898  
2899      /**
2900       * Do the database updates and return a new FileRepoStatus indicating how
2901       * many rows where updated.
2902       *
2903       * @return FileRepoStatus
2904       */
2905  	function doDBUpdates() {
2906          $repo = $this->file->repo;
2907          $status = $repo->newGood();
2908          $dbw = $this->db;
2909  
2910          // Update current image
2911          $dbw->update(
2912              'image',
2913              array( 'img_name' => $this->newName ),
2914              array( 'img_name' => $this->oldName ),
2915              __METHOD__
2916          );
2917  
2918          if ( $dbw->affectedRows() ) {
2919              $status->successCount++;
2920          } else {
2921              $status->failCount++;
2922              $status->fatal( 'imageinvalidfilename' );
2923  
2924              return $status;
2925          }
2926  
2927          // Update old images
2928          $dbw->update(
2929              'oldimage',
2930              array(
2931                  'oi_name' => $this->newName,
2932                  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
2933                      $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2934              ),
2935              array( 'oi_name' => $this->oldName ),
2936              __METHOD__
2937          );
2938  
2939          $affected = $dbw->affectedRows();
2940          $total = $this->oldCount;
2941          $status->successCount += $affected;
2942          // Bug 34934: $total is based on files that actually exist.
2943          // There may be more DB rows than such files, in which case $affected
2944          // can be greater than $total. We use max() to avoid negatives here.
2945          $status->failCount += max( 0, $total - $affected );
2946          if ( $status->failCount ) {
2947              $status->error( 'imageinvalidfilename' );
2948          }
2949  
2950          return $status;
2951      }
2952  
2953      /**
2954       * Generate triplets for FileRepo::storeBatch().
2955       * @return array
2956       */
2957  	function getMoveTriplets() {
2958          $moves = array_merge( array( $this->cur ), $this->olds );
2959          $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
2960  
2961          foreach ( $moves as $move ) {
2962              // $move: (oldRelativePath, newRelativePath)
2963              $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2964              $triplets[] = array( $srcUrl, 'public', $move[1] );
2965              wfDebugLog(
2966                  'imagemove',
2967                  "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
2968              );
2969          }
2970  
2971          return $triplets;
2972      }
2973  
2974      /**
2975       * Removes non-existent files from move batch.
2976       * @param array $triplets
2977       * @return Status
2978       */
2979  	function removeNonexistentFiles( $triplets ) {
2980          $files = array();
2981  
2982          foreach ( $triplets as $file ) {
2983              $files[$file[0]] = $file[0];
2984          }
2985  
2986          $result = $this->file->repo->fileExistsBatch( $files );
2987          if ( in_array( null, $result, true ) ) {
2988              return Status::newFatal( 'backend-fail-internal',
2989                  $this->file->repo->getBackend()->getName() );
2990          }
2991  
2992          $filteredTriplets = array();
2993          foreach ( $triplets as $file ) {
2994              if ( $result[$file[0]] ) {
2995                  $filteredTriplets[] = $file;
2996              } else {
2997                  wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
2998              }
2999          }
3000  
3001          return Status::newGood( $filteredTriplets );
3002      }
3003  
3004      /**
3005       * Cleanup a partially moved array of triplets by deleting the target
3006       * files. Called if something went wrong half way.
3007       * @param array $triplets
3008       */
3009  	function cleanupTarget( $triplets ) {
3010          // Create dest pairs from the triplets
3011          $pairs = array();
3012          foreach ( $triplets as $triplet ) {
3013              // $triplet: (old source virtual URL, dst zone, dest rel)
3014              $pairs[] = array( $triplet[1], $triplet[2] );
3015          }
3016  
3017          $this->file->repo->cleanupBatch( $pairs );
3018      }
3019  
3020      /**
3021       * Cleanup a fully moved array of triplets by deleting the source files.
3022       * Called at the end of the move process if everything else went ok.
3023       * @param array $triplets
3024       */
3025  	function cleanupSource( $triplets ) {
3026          // Create source file names from the triplets
3027          $files = array();
3028          foreach ( $triplets as $triplet ) {
3029              $files[] = $triplet[0];
3030          }
3031  
3032          $this->file->repo->cleanupBatch( $files );
3033      }
3034  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1