From 222b01f5169f1c7e69762e0e8904c24f78f71882 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 28 Jul 2010 11:52:48 +0200 Subject: update to MediaWiki 1.16.0 --- includes/filerepo/ArchivedFile.php | 68 ++++++---- includes/filerepo/FSRepo.php | 103 +++++++++++++- includes/filerepo/File.php | 76 ++++++++--- includes/filerepo/FileCache.php | 156 --------------------- includes/filerepo/FileRepo.php | 175 +++++++++++++++++------- includes/filerepo/ForeignAPIFile.php | 11 +- includes/filerepo/ForeignAPIRepo.php | 114 ++++++++++++---- includes/filerepo/ForeignDBFile.php | 10 -- includes/filerepo/ForeignDBRepo.php | 15 +++ includes/filerepo/ForeignDBViaLBRepo.php | 15 +++ includes/filerepo/Image.php | 4 +- includes/filerepo/LocalFile.php | 224 +++++++++++++++++++++---------- includes/filerepo/LocalRepo.php | 97 ++++++++----- includes/filerepo/NullRepo.php | 8 +- includes/filerepo/OldLocalFile.php | 24 ++-- includes/filerepo/RepoGroup.php | 124 +++++++++++++---- 16 files changed, 778 insertions(+), 446 deletions(-) delete mode 100644 includes/filerepo/FileCache.php (limited to 'includes/filerepo') diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index 68c93b8f..ffc06303 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -33,7 +33,7 @@ class ArchivedFile $this->id = -1; $this->title = false; $this->name = false; - $this->group = ''; + $this->group = 'deleted'; // needed for direct use of constructor $this->key = ''; $this->size = 0; $this->bits = 0; @@ -45,47 +45,48 @@ class ArchivedFile $this->description = ''; $this->user = 0; $this->user_text = ''; - $this->timestamp = NULL; + $this->timestamp = null; $this->deleted = 0; $this->dataLoaded = false; - + $this->exists = false; + if( is_object($title) ) { $this->title = $title; $this->name = $title->getDBkey(); } - + if ($id) $this->id = $id; - + if ($key) $this->key = $key; - + if (!$id && !$key && !is_object($title)) throw new MWException( "No specifications provided to ArchivedFile constructor." ); } /** * Loads a file object from the filearchive table - * @return ResultWrapper + * @return true on success or null */ public function load() { if ( $this->dataLoaded ) { return true; } $conds = array(); - + if( $this->id > 0 ) $conds['fa_id'] = $this->id; if( $this->key ) { - $conds['fa_storage_group'] = $this->group; + $conds['fa_storage_group'] = $this->group; $conds['fa_storage_key'] = $this->key; } if( $this->title ) $conds['fa_name'] = $this->title->getDBkey(); - + if( !count($conds)) throw new MWException( "No specific information for retrieving archived file" ); - + if( !$this->title || $this->title->getNamespace() == NS_FILE ) { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'filearchive', @@ -142,13 +143,14 @@ class ArchivedFile return; } $this->dataLoaded = true; + $this->exists = true; return true; } /** * Loads a file object from the filearchive table - * @return ResultWrapper + * @return ArchivedFile */ public static function newFromRow( $row ) { $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) ); @@ -176,7 +178,6 @@ class ArchivedFile /** * Return the associated title object - * @public */ public function getTitle() { return $this->title; @@ -194,6 +195,11 @@ class ArchivedFile return $this->id; } + public function exists() { + $this->load(); + return $this->exists; + } + /** * Return the FileStore key */ @@ -202,6 +208,13 @@ class ArchivedFile return $this->key; } + /** + * Return the FileStore key (overriding base File class) + */ + public function getStorageKey() { + return $this->getKey(); + } + /** * Return the FileStore storage group */ @@ -235,7 +248,6 @@ class ArchivedFile /** * Return the size of the image file, in bytes - * @public */ public function getSize() { $this->load(); @@ -244,7 +256,6 @@ class ArchivedFile /** * Return the bits of the image file, in bytes - * @public */ public function getBits() { $this->load(); @@ -337,30 +348,33 @@ class ArchivedFile } /** - * int $field one of DELETED_* bitfield constants + * Returns the deletion bitfield + * @return int + */ + public function getVisibility() { + $this->load(); + return $this->deleted; + } + + /** * for file or revision rows + * + * @param $field Integer: one of DELETED_* bitfield constants * @return bool */ public function isDeleted( $field ) { + $this->load(); return ($this->deleted & $field) == $field; } /** * Determine if the current user is allowed to view a particular * field of this FileStore image file, if it's marked as deleted. - * @param int $field + * @param $field Integer * @return bool */ public function userCan( $field ) { - if( ($this->deleted & $field) == $field ) { - global $wgUser; - $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED - ? 'suppressrevision' - : 'deleterevision'; - wfDebug( "Checking for $permission due to $field match on $this->deleted\n" ); - return $wgUser->isAllowed( $permission ); - } else { - return true; - } + $this->load(); + return Revision::userCanBitfield( $this->deleted, $field ); } } diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index d561e61b..0dd9d0f7 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -6,7 +6,7 @@ * @ingroup FileRepo */ class FSRepo extends FileRepo { - var $directory, $deletedDir, $url, $deletedHashLevels; + var $directory, $deletedDir, $deletedHashLevels, $fileMode; var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); var $oldFileFactory = false; var $pathDisclosureProtection = 'simple'; @@ -23,6 +23,17 @@ class FSRepo extends FileRepo { $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? $info['deletedHashLevels'] : $this->hashLevels; $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false; + $this->fileMode = isset( $info['fileMode'] ) ? $info['fileMode'] : 0644; + if ( isset( $info['thumbDir'] ) ) { + $this->thumbDir = $info['thumbDir']; + } else { + $this->thumbDir = "{$this->directory}/thumb"; + } + if ( isset( $info['thumbUrl'] ) ) { + $this->thumbUrl = $info['thumbUrl']; + } else { + $this->thumbUrl = "{$this->url}/thumb"; + } } /** @@ -57,13 +68,15 @@ class FSRepo extends FileRepo { return "{$this->directory}/temp"; case 'deleted': return $this->deletedDir; + case 'thumb': + return $this->thumbDir; default: return false; } } /** - * Get the URL corresponding to one of the three basic zones + * @see FileRepo::getZoneUrl() */ function getZoneUrl( $zone ) { switch ( $zone ) { @@ -72,9 +85,11 @@ class FSRepo extends FileRepo { case 'temp': return "{$this->url}/temp"; case 'deleted': - return false; // no public URL + return parent::getZoneUrl( $zone ); // no public URL + case 'thumb': + return $this->thumbUrl; default: - return false; + return parent::getZoneUrl( $zone ); } } @@ -203,7 +218,7 @@ class FSRepo extends FileRepo { } } if ( $good ) { - chmod( $dstPath, 0644 ); + $this->chmod( $dstPath ); $status->successCount++; } else { $status->failCount++; @@ -212,6 +227,70 @@ class FSRepo extends FileRepo { return $status; } + function append( $srcPath, $toAppendPath, $flags = 0 ) { + $status = $this->newGood(); + + // Resolve the virtual URL + if ( self::isVirtualUrl( $srcPath ) ) { + $srcPath = $this->resolveVirtualUrl( $srcPath ); + } + // Make sure the files are there + if ( !is_file( $srcPath ) ) + $status->fatal( 'filenotfound', $srcPath ); + + if ( !is_file( $toAppendPath ) ) + $status->fatal( 'filenotfound', $toAppendPath ); + + if ( !$status->isOk() ) return $status; + + // Do the append + $chunk = file_get_contents( $toAppendPath ); + if( $chunk === false ) { + $status->fatal( 'fileappenderrorread', $toAppendPath ); + } + + if( $status->isOk() ) { + if ( file_put_contents( $srcPath, $chunk, FILE_APPEND ) ) { + $status->value = $srcPath; + } else { + $status->fatal( 'fileappenderror', $toAppendPath, $srcPath); + } + } + + if ( $flags & self::DELETE_SOURCE ) { + unlink( $toAppendPath ); + } + + return $status; + } + + /** + * Checks existence of specified array of files. + * + * @param array $files URLs of files to check + * @param integer $flags Bitwise combination of the following flags: + * self::FILES_ONLY Mark file as existing only if it is a file (not directory) + * @return Either array of files and existence flags, or false + */ + function fileExistsBatch( $files, $flags = 0 ) { + if ( !file_exists( $this->directory ) || !is_readable( $this->directory ) ) { + return false; + } + $result = array(); + foreach ( $files as $key => $file ) { + if ( self::isVirtualUrl( $file ) ) { + $file = $this->resolveVirtualUrl( $file ); + } + if( $flags & self::FILES_ONLY ) { + $result[$key] = is_file( $file ); + } else { + $result[$key] = file_exists( $file ); + } + } + + return $result; + } + /** * Take all available measures to prevent web accessibility of new deleted * directories, in case the user has not configured offline storage @@ -362,7 +441,7 @@ class FSRepo extends FileRepo { $status->successCount++; wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n"); // Thread-safe override for umask - chmod( $dstPath, 0644 ); + $this->chmod( $dstPath ); } else { $status->failCount++; } @@ -439,7 +518,7 @@ class FSRepo extends FileRepo { $status->error( 'filerenameerror', $srcPath, $archivePath ); $good = false; } else { - @chmod( $archivePath, 0644 ); + $this->chmod( $archivePath ); } } if ( $good ) { @@ -534,4 +613,14 @@ class FSRepo extends FileRepo { return strtr( $param, $this->simpleCleanPairs ); } + /** + * Chmod a file, supressing the warnings. + * @param String $path The path to change + */ + protected function chmod( $path ) { + wfSuppressWarnings(); + chmod( $path, $this->fileMode ); + wfRestoreWarnings(); + } + } diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 523a1c09..d79a1661 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -529,7 +529,7 @@ abstract class File { * @return MediaTransformOutput */ function transform( $params, $flags = 0 ) { - global $wgUseSquid, $wgIgnoreImageErrors; + global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgServer; wfProfileIn( __METHOD__ ); do { @@ -539,6 +539,12 @@ abstract class File { break; } + // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791. + $descriptionUrl = $this->getDescriptionUrl(); + if ( $descriptionUrl ) { + $params['descriptionUrl'] = $wgServer . $descriptionUrl; + } + $script = $this->getTransformScript(); if ( $script && !($flags & self::RENDER_NOW) ) { // Use a script to transform on client request, if possible @@ -561,9 +567,14 @@ abstract class File { wfDebug( __METHOD__.": Doing stat for $thumbPath\n" ); $this->migrateThumbFile( $thumbName ); - if ( file_exists( $thumbPath ) ) { - $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); - break; + if ( file_exists( $thumbPath )) { + $thumbTime = filemtime( $thumbPath ); + if ( $thumbTime !== FALSE && + gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) { + + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } } $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); @@ -746,15 +757,6 @@ abstract class File { return $path; } - /** Get relative path for a thumbnail file */ - function getThumbRel( $suffix = false ) { - $path = 'thumb/' . $this->getRel(); - if ( $suffix !== false ) { - $path .= '/' . $suffix; - } - return $path; - } - /** Get the path of the archive directory, or a particular file if $suffix is specified */ function getArchivePath( $suffix = false ) { return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix ); @@ -762,7 +764,11 @@ abstract class File { /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ function getThumbPath( $suffix = false ) { - return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix ); + $path = $this->repo->getZonePath('thumb') . '/' . $this->getRel(); + if ( $suffix !== false ) { + $path .= '/' . $suffix; + } + return $path; } /** Get the URL of the archive directory, or a particular file if $suffix is specified */ @@ -778,7 +784,7 @@ abstract class File { /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */ function getThumbUrl( $suffix = false ) { - $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel(); + $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel(); if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } @@ -798,7 +804,7 @@ abstract class File { /** Get the virtual URL for a thumbnail file or directory */ function getThumbVirtualUrl( $suffix = false ) { - $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel(); + $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel(); if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } @@ -943,6 +949,14 @@ abstract class File { function isDeleted( $field ) { return false; } + + /** + * Return the deletion bitfield + * STUB + */ + function getVisibility() { + return 0; + } /** * Was this file ever deleted from the wiki? @@ -1007,8 +1021,9 @@ abstract class File { } /** - * Returns 'true' if this image is a multipage document, e.g. a DJVU - * document. + * Returns 'true' if this file is a type which supports multiple pages, + * e.g. DJVU or PDF. Note that this may be true even if the file in + * question only has a single page. * * @return Bool */ @@ -1069,15 +1084,15 @@ abstract class File { * Get the HTML text of the description page, if available */ function getDescriptionText() { - global $wgMemc, $wgContLang; + global $wgMemc, $wgLang; if ( !$this->repo->fetchDescription ) { return false; } - $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() ); + $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() ); if ( $renderUrl ) { if ( $this->repo->descriptionCacheExpiry > 0 ) { wfDebug("Attempting to get the description from cache..."); - $key = wfMemcKey( 'RemoteFileDescription', 'url', $wgContLang->getCode(), + $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(), $this->getName() ); $obj = $wgMemc->get($key); if ($obj) { @@ -1124,6 +1139,19 @@ abstract class File { return self::sha1Base36( $this->getPath() ); } + /** + * Get the deletion archive key, . + */ + function getStorageKey() { + $hash = $this->getSha1(); + if ( !$hash ) { + return false; + } + $ext = $this->getExtension(); + $dotExt = $ext === '' ? '' : ".$ext"; + return $hash . $dotExt; + } + /** * Determine if the current user is allowed to view a particular * field of this file, if it's marked as deleted. @@ -1173,7 +1201,7 @@ abstract class File { wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n"); } else { - $info['mime'] = NULL; + $info['mime'] = null; $info['media_type'] = MEDIATYPE_UNKNOWN; $info['metadata'] = ''; $info['sha1'] = ''; @@ -1259,6 +1287,10 @@ abstract class File { function redirectedFrom( $from ) { $this->redirected = $from; } + + function isMissing() { + return false; + } } /** * Aliases for backwards compatibility with 1.6 diff --git a/includes/filerepo/FileCache.php b/includes/filerepo/FileCache.php deleted file mode 100644 index 7840d1a3..00000000 --- a/includes/filerepo/FileCache.php +++ /dev/null @@ -1,156 +0,0 @@ -repoGroup = $repoGroup; - } - - - /** - * Add some files to the cache. This is a fairly low-level function, - * which most users should not need to call. Note that any existing - * entries for the same keys will not be replaced. Call clearFiles() - * first if you need that. - * @param array $files array of File objects, indexed by DB key - */ - function addFiles( $files ) { - wfDebug( "FileCache adding ".count( $files )." files\n" ); - $this->cache += $files; - } - - /** - * Remove some files from the cache, so that their existence will be - * rechecked. This is a fairly low-level function, which most users - * should not need to call. - * @param array $remove array indexed by DB keys to remove (the values are ignored) - */ - function clearFiles( $remove ) { - wfDebug( "FileCache clearing data for ".count( $remove )." files\n" ); - $this->cache = array_diff_keys( $this->cache, $remove ); - $this->notFound = array_diff_keys( $this->notFound, $remove ); - } - - /** - * Mark some DB keys as nonexistent. This is a fairly low-level - * function, which most users should not need to call. - * @param array $dbkeys array of DB keys - */ - function markNotFound( $dbkeys ) { - wfDebug( "FileCache marking ".count( $dbkeys )." files as not found\n" ); - $this->notFound += array_fill_keys( $dbkeys, true ); - } - - - /** - * Search the cache for a file. - * @param mixed $title Title object or string - * @return File object or false if it is not found - * @todo Implement searching for old file versions(?) - */ - function findFile( $title ) { - if( !( $title instanceof Title ) ) { - $title = Title::makeTitleSafe( NS_FILE, $title ); - } - if( !$title ) { - return false; // invalid title? - } - - $dbkey = $title->getDBkey(); - if( array_key_exists( $dbkey, $this->cache ) ) { - wfDebug( "FileCache HIT for $dbkey\n" ); - return $this->cache[$dbkey]; - } - if( array_key_exists( $dbkey, $this->notFound ) ) { - wfDebug( "FileCache negative HIT for $dbkey\n" ); - return false; - } - - // Not in cache, fall back to a direct query - $file = $this->repoGroup->findFile( $title ); - if( $file ) { - wfDebug( "FileCache MISS for $dbkey\n" ); - $this->cache[$dbkey] = $file; - } else { - wfDebug( "FileCache negative MISS for $dbkey\n" ); - $this->notFound[$dbkey] = true; - } - return $file; - } - - /** - * Search the cache for multiple files. - * @param array $titles Title objects or strings to search for - * @return array of File objects, indexed by DB key - */ - function findFiles( $titles ) { - $titleObjs = array(); - foreach ( $titles as $title ) { - if ( !( $title instanceof Title ) ) { - $title = Title::makeTitleSafe( NS_FILE, $title ); - } - if ( $title ) { - $titleObjs[$title->getDBkey()] = $title; - } - } - - $result = array_intersect_key( $this->cache, $titleObjs ); - - $unsure = array_diff_key( $titleObjs, $result, $this->notFound ); - if( $unsure ) { - wfDebug( "FileCache MISS for ".count( $unsure )." files out of ".count( $titleObjs )."...\n" ); - // XXX: We assume the array returned by findFiles() is - // indexed by DBkey; this appears to be true, but should - // be explicitly documented. - $found = $this->repoGroup->findFiles( $unsure ); - $result += $found; - $this->addFiles( $found ); - $this->markNotFound( array_keys( array_diff_key( $unsure, $found ) ) ); - } - - wfDebug( "FileCache found ".count( $result )." files out of ".count( $titleObjs )."\n" ); - return $result; - } -} diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index c9d34377..f94709b3 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -6,16 +6,15 @@ * @ingroup FileRepo */ abstract class FileRepo { + const FILES_ONLY = 1; const DELETE_SOURCE = 1; - const FIND_PRIVATE = 1; - const FIND_IGNORE_REDIRECT = 2; const OVERWRITE = 2; const OVERWRITE_SAME = 4; var $thumbScriptUrl, $transformVia404; var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; var $pathDisclosureProtection = 'paranoid'; - var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels; + var $descriptionCacheExpiry, $hashLevels, $url, $thumbUrl; /** * Factory functions for creating new files @@ -29,10 +28,10 @@ abstract class FileRepo { $this->name = $info['name']; // Optional settings - $this->initialCapital = true; // by default + $this->initialCapital = MWNamespace::isCapitalized( NS_FILE ); foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', - 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', - 'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var ) + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', + 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl' ) as $var ) { if ( isset( $info[$var] ) ) { $this->$var = $info[$var]; @@ -81,9 +80,24 @@ abstract class FileRepo { * version control should return false if the time is specified. * * @param mixed $title Title object or string - * @param mixed $time 14-character timestamp, or false for the current version - */ - function findFile( $title, $time = false, $flags = 0 ) { + * @param $options Associative array of options: + * time: requested time for an archived image, or false for the + * current version. An image object will be returned which was + * created at the specified time. + * + * ignoreRedirect: If true, do not follow file redirects + * + * private: If true, return restricted (deleted) files if the current + * user is allowed to view them. Otherwise, such files will not + * be found. + */ + function findFile( $title, $options = array() ) { + if ( !is_array( $options ) ) { + // MW 1.15 compat + $time = $options; + } else { + $time = isset( $options['time'] ) ? $options['time'] : false; + } if ( !($title instanceof Title) ) { $title = Title::makeTitleSafe( NS_FILE, $title ); if ( !is_object( $title ) ) { @@ -104,17 +118,17 @@ abstract class FileRepo { if ( $img && $img->exists() ) { if ( !$img->isDeleted(File::DELETED_FILE) ) { return $img; - } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) { + } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) { return $img; } } } - + # Now try redirects - if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) { + if ( !empty( $options['ignoreRedirect'] ) ) { return false; } - $redir = $this->checkRedirect( $title ); + $redir = $this->checkRedirect( $title ); if( $redir && $redir->getNamespace() == NS_FILE) { $img = $this->newFile( $redir ); if( !$img ) { @@ -127,22 +141,34 @@ abstract class FileRepo { } return false; } - + /* - * Find many files at once. - * @param array $titles, an array of titles - * @todo Think of a good way to optionally pass timestamps to this function. + * Find many files at once. + * @param array $items, an array of titles, or an array of findFile() options with + * the "title" option giving the title. Example: + * + * $findItem = array( 'title' => $title, 'private' => true ); + * $findBatch = array( $findItem ); + * $repo->findFiles( $findBatch ); */ - function findFiles( $titles ) { + function findFiles( $items ) { $result = array(); - foreach ( $titles as $index => $title ) { - $file = $this->findFile( $title ); + foreach ( $items as $index => $item ) { + if ( is_array( $item ) ) { + $title = $item['title']; + $options = $item; + unset( $options['title'] ); + } else { + $title = $item; + $options = array(); + } + $file = $this->findFile( $title, $options ); if ( $file ) $result[$file->getTitle()->getDBkey()] = $file; } return $result; } - + /** * Create a new File object from the local repository * @param mixed $sha1 SHA-1 key @@ -163,16 +189,23 @@ abstract class FileRepo { return call_user_func( $this->fileFactoryKey, $sha1, $this ); } } - + /** * Find an instance of the file with this key, created at the specified time * Returns false if the file does not exist. Repositories not supporting * version control should return false if the time is specified. * * @param string $sha1 string - * @param mixed $time 14-character timestamp, or false for the current version + * @param array $options Option array, same as findFile(). */ - function findFileFromKey( $sha1, $time = false, $flags = 0 ) { + function findFileFromKey( $sha1, $options = array() ) { + if ( !is_array( $options ) ) { + # MW 1.15 compat + $time = $options; + } else { + $time = isset( $options['time'] ) ? $options['time'] : false; + } + # First try the current version of the file to see if it precedes the timestamp $img = $this->newFileFromKey( $sha1 ); if ( !$img ) { @@ -187,7 +220,7 @@ abstract class FileRepo { if ( $img->exists() ) { if ( !$img->isDeleted(File::DELETED_FILE) ) { return $img; - } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) { + } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) { return $img; } } @@ -202,6 +235,15 @@ abstract class FileRepo { return $this->thumbScriptUrl; } + /** + * Get the URL corresponding to one of the four basic zones + * @param String $zone One of: public, deleted, temp, thumb + * @return String or false + */ + function getZoneUrl( $zone ) { + return false; + } + /** * Returns true if the repository can transform files via a 404 handler */ @@ -214,7 +256,7 @@ abstract class FileRepo { */ function getNameFromTitle( $title ) { global $wgCapitalLinks; - if ( $this->initialCapital != $wgCapitalLinks ) { + if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { global $wgContLang; $name = $title->getUserCaseDBKey(); if ( $this->initialCapital ) { @@ -238,7 +280,7 @@ abstract class FileRepo { return $path; } } - + /** * Get a relative path including trailing slash, e.g. f/fa/ * If the repo is not hashed, returns an empty string @@ -355,6 +397,17 @@ abstract class FileRepo { */ abstract function storeTemp( $originalName, $srcPath ); + + /** + * Append the contents of the source path to the given file. + * @param $srcPath string location of the source file + * @param $toAppendPath string path to append to. + * @param $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source file should be deleted if possible + * @return mixed Status or false + */ + abstract function append( $srcPath, $toAppendPath, $flags = 0 ); + /** * Remove a temporary file or mark it for garbage collection * @param string $virtualUrl The virtual URL returned by storeTemp @@ -400,6 +453,21 @@ abstract class FileRepo { */ abstract function publishBatch( $triplets, $flags = 0 ); + function fileExists( $file, $flags = 0 ) { + $result = $this->fileExistsBatch( array( $file ), $flags ); + return $result[0]; + } + + /** + * Checks existence of an array of files. + * + * @param array $files URLs (or paths) of files to check + * @param integer $flags Bitwise combination of the following flags: + * self::FILES_ONLY Mark file as existing only if it is a file (not directory) + * @return Either array of files and existence flags, or false + */ + abstract function fileExistsBatch( $files, $flags = 0 ); + /** * Move a group of files to the deletion archive. * @@ -529,21 +597,25 @@ abstract class FileRepo { /** * Invalidates image redirect cache related to that image + * Doesn't do anything for repositories that don't support image redirects. * + * STUB * @param Title $title Title of image - */ - function invalidateImageRedirect( $title ) { - global $wgMemc; - $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) ); - $wgMemc->delete( $memcKey ); - } - + */ + function invalidateImageRedirect( $title ) {} + + /** + * Get an array or iterator of file objects for files that have a given + * SHA-1 content hash. + * + * STUB + */ function findBySha1( $hash ) { return array(); } - + /** - * Get the human-readable name of the repo. + * Get the human-readable name of the repo. * @return string */ public function getDisplayName() { @@ -551,22 +623,33 @@ abstract class FileRepo { if ( $this->name == 'local' ) { return null; } + // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true $repoName = wfMsg( 'shared-repo-name-' . $this->name ); if ( !wfEmptyMsg( 'shared-repo-name-' . $this->name, $repoName ) ) { return $repoName; } - return wfMsg( 'shared-repo' ); - } - - function getSlaveDB() { - return wfGetDB( DB_SLAVE ); + return wfMsg( 'shared-repo' ); } - function getMasterDB() { - return wfGetDB( DB_MASTER ); + /** + * Get a key on the primary cache for this repository. + * Returns false if the repository's cache is not accessible at this site. + * The parameters are the parts of the key, as for wfMemcKey(). + * + * STUB + */ + function getSharedCacheKey( /*...*/ ) { + return false; } - - function getMemcKey( $key ) { - return wfWikiID( $this->getSlaveDB() ) . ":{$key}"; + + /** + * Get a key for this repo in the local cache domain. These cache keys are + * not shared with remote instances of the repo. + * The parameters are the parts of the key, as for wfMemcKey(). + */ + function getLocalCacheKey( /*...*/ ) { + $args = func_get_args(); + array_unshift( $args, 'filerepo', $this->getName() ); + return call_user_func_array( 'wfMemcKey', $args ); } } diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php index 03498fb1..c46b1f8f 100644 --- a/includes/filerepo/ForeignAPIFile.php +++ b/includes/filerepo/ForeignAPIFile.php @@ -43,10 +43,7 @@ class ForeignAPIFile extends File { $this->getName(), isset( $params['width'] ) ? $params['width'] : -1, isset( $params['height'] ) ? $params['height'] : -1 ); - if( $thumbUrl ) { - return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );; - } - return false; + return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );; } // Info we can get from API... @@ -108,7 +105,7 @@ class ForeignAPIFile extends File { return $this->mInfo['mime']; } - /// @fixme May guess wrong on file types that can be eg audio or video + /// @todo Fixme: may guess wrong on file types that can be eg audio or video function getMediaType() { $magic = MimeMagic::singleton(); return $magic->getMediaType( null, $this->getMimeType() ); @@ -162,13 +159,13 @@ class ForeignAPIFile extends File { function purgeDescriptionPage() { global $wgMemc, $wgContLang; $url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() ); - $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($url) ); + $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) ); $wgMemc->delete( $key ); } function purgeThumbnails() { global $wgMemc; - $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() ); + $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() ); $wgMemc->delete( $key ); $files = $this->getThumbnails(); $dir = $this->getThumbPath( $this->getName() ); diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index e63e4a6b..264cb920 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -19,18 +19,30 @@ */ class ForeignAPIRepo extends FileRepo { var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); - var $apiThumbCacheExpiry = 0; + var $apiThumbCacheExpiry = 86400; protected $mQueryCache = array(); - + protected $mFileExists = array(); + function __construct( $info ) { parent::__construct( $info ); $this->mApiBase = $info['apibase']; // http://commons.wikimedia.org/w/api.php + if( isset( $info['apiThumbCacheExpiry'] ) ) { + $this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry']; + } if( !$this->scriptDirUrl ) { // hack for description fetches $this->scriptDirUrl = dirname( $this->mApiBase ); } + // If we can cache thumbs we can guess sane defaults for these + if( $this->canCacheThumbs() && !$this->url ) { + global $wgLocalFileRepo; + $this->url = $wgLocalFileRepo['url']; + } + if( $this->canCacheThumbs() && !$this->thumbUrl ) { + $this->thumbUrl = $this->url . '/thumb'; + } } - + /** * Per docs in FileRepo, this needs to return false if we don't support versioned * files. Well, we don't. @@ -51,19 +63,49 @@ class ForeignAPIRepo extends FileRepo { function storeTemp( $originalName, $srcPath ) { return false; } + function append( $srcPath, $toAppendPath, $flags = 0 ){ + return false; + } function publishBatch( $triplets, $flags = 0 ) { return false; } function deleteBatch( $sourceDestPairs ) { return false; } + + + function fileExistsBatch( $files, $flags = 0 ) { + $results = array(); + foreach ( $files as $k => $f ) { + if ( isset( $this->mFileExists[$k] ) ) { + $results[$k] = true; + unset( $files[$k] ); + } elseif( self::isVirtualUrl( $f ) ) { + # TODO! FIXME! We need to be able to handle virtual + # URLs better, at least when we know they refer to the + # same repo. + $results[$k] = false; + unset( $files[$k] ); + } + } + + $results = $this->fetchImageQuery( array( 'titles' => implode( $files, '|' ), + 'prop' => 'imageinfo' ) ); + if( isset( $data['query']['pages'] ) ) { + $i = 0; + foreach( $files as $key => $file ) { + $results[$key] = $this->mFileExists[$key] = !isset( $data['query']['pages'][$i]['missing'] ); + $i++; + } + } + } function getFileProps( $virtualUrl ) { return false; } - + protected function queryImage( $query ) { $data = $this->fetchImageQuery( $query ); - + if( isset( $data['query']['pages'] ) ) { foreach( $data['query']['pages'] as $pageid => $info ) { if( isset( $info['imageinfo'][0] ) ) { @@ -73,10 +115,10 @@ class ForeignAPIRepo extends FileRepo { } return false; } - + protected function fetchImageQuery( $query ) { global $wgMemc; - + $url = $this->mApiBase . '?' . wfArrayToCgi( @@ -84,9 +126,9 @@ class ForeignAPIRepo extends FileRepo { array( 'format' => 'json', 'action' => 'query' ) ) ); - + if( !isset( $this->mQueryCache[$url] ) ) { - $key = wfMemcKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) ); + $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) ); $data = $wgMemc->get( $key ); if( !$data ) { $data = Http::get( $url ); @@ -102,16 +144,16 @@ class ForeignAPIRepo extends FileRepo { } $this->mQueryCache[$url] = $data; } - return json_decode( $this->mQueryCache[$url], true ); + return FormatJson::decode( $this->mQueryCache[$url], true ); } - + function getImageInfo( $title, $time = false ) { return $this->queryImage( array( 'titles' => 'Image:' . $title->getText(), 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime', 'prop' => 'imageinfo' ) ); } - + function findBySha1( $hash ) { $results = $this->fetchImageQuery( array( 'aisha1base36' => $hash, @@ -125,7 +167,7 @@ class ForeignAPIRepo extends FileRepo { } return $ret; } - + function getThumbUrl( $name, $width=-1, $height=-1 ) { $info = $this->queryImage( array( 'titles' => 'Image:' . $name, @@ -133,49 +175,69 @@ class ForeignAPIRepo extends FileRepo { 'iiurlwidth' => $width, 'iiurlheight' => $height, 'prop' => 'imageinfo' ) ); - if( $info ) { + if( $info && $info['thumburl'] ) { wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" ); return $info['thumburl']; } else { return false; } } - + function getThumbUrlFromCache( $name, $width, $height ) { global $wgMemc, $wgUploadPath, $wgServer, $wgUploadDirectory; - + if ( !$this->canCacheThumbs() ) { return $this->getThumbUrl( $name, $width, $height ); } - - $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $name ); + + $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $name ); if ( $thumbUrl = $wgMemc->get($key) ) { wfDebug("Got thumb from local cache. $thumbUrl \n"); return $thumbUrl; } else { $foreignUrl = $this->getThumbUrl( $name, $width, $height ); - + if( !$foreignUrl ) { + wfDebug( __METHOD__ . " Could not find thumburl\n" ); + return false; + } + $thumb = Http::get( $foreignUrl ); + if( !$thumb ) { + wfDebug( __METHOD__ . " Could not download thumb\n" ); + return false; + } // We need the same filename as the remote one :) - $fileName = ltrim( substr( $foreignUrl, strrpos( $foreignUrl, '/' ) ), '/' ); + $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) ); $path = 'thumb/' . $this->getHashPath( $name ) . $name . "/"; if ( !is_dir($wgUploadDirectory . '/' . $path) ) { wfMkdirParents($wgUploadDirectory . '/' . $path); } - if ( !is_writable( $wgUploadDirectory . '/' . $path . $fileName ) ) { + $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName; + # FIXME: Delete old thumbs that aren't being used. Maintenance script? + if( !file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb ) ) { wfDebug( __METHOD__ . " could not write to thumb path\n" ); return $foreignUrl; } - $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName; - $thumb = Http::get( $foreignUrl ); - # FIXME: Delete old thumbs that aren't being used. Maintenance script? - file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb ); $wgMemc->set( $key, $localUrl, $this->apiThumbCacheExpiry ); wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); return $localUrl; } } - + + /** + * @see FileRepo::getZoneUrl() + */ + function getZoneUrl( $zone ) { + switch ( $zone ) { + case 'public': + return $this->url; + case 'thumb': + return $this->thumbUrl; + default: + return parent::getZoneUrl( $zone ); + } + } + /** * Are we locally caching the thumbnails? * @return bool diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php index 8fe6f921..a24ff72b 100644 --- a/includes/filerepo/ForeignDBFile.php +++ b/includes/filerepo/ForeignDBFile.php @@ -19,16 +19,6 @@ class ForeignDBFile extends LocalFile { return $file; } - function getCacheKey() { - if ( $this->repo->hasSharedCache() ) { - $hashedName = md5($this->name); - return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix, - 'file', $hashedName ); - } else { - return false; - } - } - function publish( $srcPath, $flags = 0 ) { $this->readOnlyError(); } diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index e078dd25..35c2c4bf 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -44,6 +44,21 @@ class ForeignDBRepo extends LocalRepo { return $this->hasSharedCache; } + /** + * Get a key on the primary cache for this repository. + * Returns false if the repository's cache is not accessible at this site. + * The parameters are the parts of the key, as for wfMemcKey(). + */ + function getSharedCacheKey( /*...*/ ) { + if ( $this->hasSharedCache() ) { + $args = func_get_args(); + array_unshift( $args, $this->dbName, $this->tablePrefix ); + return call_user_func_array( 'wfForeignMemcKey', $args ); + } else { + return false; + } + } + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { throw new MWException( get_class($this) . ': write operations are not supported' ); } diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index 13c9f434..80325752 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -27,6 +27,21 @@ class ForeignDBViaLBRepo extends LocalRepo { return $this->hasSharedCache; } + /** + * Get a key on the primary cache for this repository. + * Returns false if the repository's cache is not accessible at this site. + * The parameters are the parts of the key, as for wfMemcKey(). + */ + function getSharedCacheKey( /*...*/ ) { + if ( $this->hasSharedCache() ) { + $args = func_get_args(); + array_unshift( $args, $this->wiki ); + return implode( ':', $args ); + } else { + return false; + } + } + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { throw new MWException( get_class($this) . ': write operations are not supported' ); } diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php index 5207bb4b..08ce219a 100644 --- a/includes/filerepo/Image.php +++ b/includes/filerepo/Image.php @@ -19,7 +19,7 @@ class Image extends LocalFile { */ static function newFromTitle( $title, $time = false ) { wfDeprecated( __METHOD__ ); - $img = wfFindFile( $title, $time ); + $img = wfFindFile( $title, array( 'time' => $time ) ); if ( !$img ) { $img = wfLocalFile( $title ); } @@ -44,7 +44,7 @@ class Image extends LocalFile { } return $img; } else { - return NULL; + return null; } } diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index b997d75f..b6b4bfed 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -24,8 +24,7 @@ define( 'MW_FILE_VERSION', 8 ); * * @ingroup FileRepo */ -class LocalFile extends File -{ +class LocalFile extends File { /**#@+ * @private */ @@ -49,6 +48,7 @@ class LocalFile extends File $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) $upgraded, # Whether the row was upgraded on load $locked, # True if the image row is locked + $missing, # True if file is not present in file system. Not to be cached in memcached $deleted; # Bitfield akin to rev_deleted /**#@-*/ @@ -122,7 +122,7 @@ class LocalFile extends File */ function __construct( $title, $repo ) { if( !is_object( $title ) ) { - throw new MWException( __CLASS__.' constructor given bogus title.' ); + throw new MWException( __CLASS__ . ' constructor given bogus title.' ); } parent::__construct( $title, $repo ); $this->metadata = ''; @@ -132,11 +132,12 @@ class LocalFile extends File } /** - * Get the memcached key + * Get the memcached key for the main data for this file, or false if + * there is no access to the shared cache. */ function getCacheKey() { - $hashedName = md5($this->getName()); - return wfMemcKey( 'file', $hashedName ); + $hashedName = md5( $this->getName() ); + return $this->repo->getSharedCacheKey( 'file', $hashedName ); } /** @@ -148,12 +149,13 @@ class LocalFile extends File $this->dataLoaded = false; $key = $this->getCacheKey(); if ( !$key ) { + wfProfileOut( __METHOD__ ); return false; } $cachedValues = $wgMemc->get( $key ); // Check if the key existed and belongs to this version of MediaWiki - if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) { + if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) { wfDebug( "Pulling file metadata from cache key $key\n" ); $this->fileExists = $cachedValues['fileExists']; if ( $this->fileExists ) { @@ -250,7 +252,7 @@ class LocalFile extends File $prefixLength = strlen( $prefix ); // Sanity check prefix once if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) { - throw new MWException( __METHOD__. ': incorrect $prefix parameter' ); + throw new MWException( __METHOD__ . ': incorrect $prefix parameter' ); } $decoded = array(); foreach ( $array as $name => $value ) { @@ -258,19 +260,19 @@ class LocalFile extends File } $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] ); if ( empty( $decoded['major_mime'] ) ) { - $decoded['mime'] = "unknown/unknown"; + $decoded['mime'] = 'unknown/unknown'; } else { - if (!$decoded['minor_mime']) { - $decoded['minor_mime'] = "unknown"; + if ( !$decoded['minor_mime'] ) { + $decoded['minor_mime'] = 'unknown'; } - $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime']; + $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime']; } # Trim zero padding from char/binary field $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); return $decoded; } - /* + /** * Load file metadata from a DB result row */ function loadFromRow( $row, $prefix = 'img_' ) { @@ -303,7 +305,7 @@ class LocalFile extends File if ( wfReadOnly() ) { return; } - if ( is_null($this->media_type) || + if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) { $this->upgradeRow(); @@ -331,16 +333,18 @@ class LocalFile extends File # Don't destroy file info of missing files if ( !$this->fileExists ) { - wfDebug( __METHOD__.": file does not exist, aborting\n" ); + wfDebug( __METHOD__ . ": file does not exist, aborting\n" ); + wfProfileOut( __METHOD__ ); return; } $dbw = $this->repo->getMasterDB(); list( $major, $minor ) = self::splitMime( $this->mime ); if ( wfReadOnly() ) { + wfProfileOut( __METHOD__ ); return; } - wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n"); + wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" ); $dbw->update( 'image', array( @@ -391,13 +395,20 @@ class LocalFile extends File /** getPath inherited */ /** isVisible inhereted */ + function isMissing() { + if( $this->missing === null ) { + list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY ); + $this->missing = !$fileExists; + } + return $this->missing; + } + /** * Return the width of the image * * Returns false on error - * @public */ - function getWidth( $page = 1 ) { + public function getWidth( $page = 1 ) { $this->load(); if ( $this->isMultipage() ) { $dim = $this->getHandler()->getPageDimensions( $this, $page ); @@ -415,9 +426,8 @@ class LocalFile extends File * Return the height of the image * * Returns false on error - * @public */ - function getHeight( $page = 1 ) { + public function getHeight( $page = 1 ) { $this->load(); if ( $this->isMultipage() ) { $dim = $this->getHandler()->getPageDimensions( $this, $page ); @@ -436,7 +446,7 @@ class LocalFile extends File * * @param $type string 'text' or 'id' */ - function getUser($type='text') { + function getUser( $type = 'text' ) { $this->load(); if( $type == 'text' ) { return $this->user_text; @@ -460,9 +470,8 @@ class LocalFile extends File /** * Return the size of the image file, in bytes - * @public */ - function getSize() { + public function getSize() { $this->load(); return $this->size; } @@ -493,9 +502,8 @@ class LocalFile extends File /** * Returns true if the file file exists on disk. * @return boolean Whether file file exist on disk. - * @public */ - function exists() { + public function exists() { $this->load(); return $this->fileExists; } @@ -518,7 +526,7 @@ class LocalFile extends File // This happened occasionally due to broken migration code in 1.5 // Rename to broken-* for ( $i = 0; $i < 100 ; $i++ ) { - $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName"; + $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName"; if ( !file_exists( $broken ) ) { rename( $thumbPath, $broken ); break; @@ -551,7 +559,7 @@ class LocalFile extends File $handle = opendir( $dir ); if ( $handle ) { - while ( false !== ( $file = readdir($handle) ) ) { + while ( false !== ( $file = readdir( $handle ) ) ) { if ( $file{0} != '.' ) { $files[] = $file; } @@ -577,9 +585,11 @@ class LocalFile extends File */ function purgeHistory() { global $wgMemc; - $hashedName = md5($this->getName()); - $oldKey = wfMemcKey( 'oldfile', $hashedName ); - $wgMemc->delete( $oldKey ); + $hashedName = md5( $this->getName() ); + $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName ); + if ( $oldKey ) { + $wgMemc->delete( $oldKey ); + } } /** @@ -624,13 +634,13 @@ class LocalFile extends File /** purgeDescription inherited */ /** purgeEverything inherited */ - function getHistory($limit = null, $start = null, $end = null, $inc = true) { + function getHistory( $limit = null, $start = null, $end = null, $inc = true ) { $dbr = $this->repo->getSlaveDB(); - $tables = array('oldimage'); + $tables = array( 'oldimage' ); $fields = OldLocalFile::selectFields(); $conds = $opts = $join_conds = array(); - $eq = $inc ? "=" : ""; - $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() ); + $eq = $inc ? '=' : ''; + $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() ); if( $start ) { $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) ); } @@ -641,19 +651,19 @@ class LocalFile extends File $opts['LIMIT'] = $limit; } // Search backwards for time > x queries - $order = (!$start && $end !== null) ? "ASC" : "DESC"; + $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC'; $opts['ORDER BY'] = "oi_timestamp $order"; - $opts['USE INDEX'] = array('oldimage' => 'oi_name_timestamp'); - + $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' ); + wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, &$conds, &$opts, &$join_conds ) ); - + $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds ); $r = array(); - while( $row = $dbr->fetchObject($res) ) { - $r[] = OldLocalFile::newFromRow($row, $this->repo); + while( $row = $dbr->fetchObject( $res ) ) { + $r[] = OldLocalFile::newFromRow( $row, $this->repo ); } - if( $order == "ASC" ) { + if( $order == 'ASC' ) { $r = array_reverse( $r ); // make sure it ends up descending } return $r; @@ -666,10 +676,8 @@ class LocalFile extends File * 0 return line for current version * 1 query for old versions, return first one * 2, ... return next old version from above query - * - * @public */ - function nextHistoryLine() { + public function nextHistoryLine() { # Polymorphic function name to distinguish foreign and local fetches $fname = get_class( $this ) . '::' . __FUNCTION__; @@ -687,12 +695,12 @@ class LocalFile extends File $fname ); if ( 0 == $dbr->numRows( $this->historyRes ) ) { - $dbr->freeResult($this->historyRes); + $dbr->freeResult( $this->historyRes ); $this->historyRes = null; - return FALSE; + return false; } - } else if ( $this->historyLine == 1 ) { - $dbr->freeResult($this->historyRes); + } elseif ( $this->historyLine == 1 ) { + $dbr->freeResult( $this->historyRes ); $this->historyRes = $dbr->select( 'oldimage', '*', array( 'oi_name' => $this->title->getDBkey() ), $fname, @@ -706,12 +714,11 @@ class LocalFile extends File /** * Reset the history pointer to the first element of the history - * @public */ - function resetHistory() { + public function resetHistory() { $this->historyLine = 0; - if (!is_null($this->historyRes)) { - $this->repo->getSlaveDB()->freeResult($this->historyRes); + if ( !is_null( $this->historyRes ) ) { + $this->repo->getSlaveDB()->freeResult( $this->historyRes ); $this->historyRes = null; } } @@ -763,7 +770,7 @@ class LocalFile extends File function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false ) { - $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source ); + $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source ); if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) { return false; } @@ -804,7 +811,7 @@ class LocalFile extends File // Fail now if the file isn't there if ( !$this->fileExists ) { - wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" ); + wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" ); return false; } @@ -905,7 +912,7 @@ class LocalFile extends File $log->getRcComment(), false ); $nullRevision->insertOn( $dbw ); - wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $user) ); + wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) ); $article->updateRevisionOn( $dbw, $nullRevision ); # Invalidate the cache for the description page @@ -922,7 +929,7 @@ class LocalFile extends File # Commit the transaction now, in case something goes wrong later # The most important thing is that files don't get lost, especially archives - $dbw->immediateCommit(); + $dbw->commit(); # Invalidate cache for all pages using this file $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); @@ -1059,7 +1066,7 @@ class LocalFile extends File * * @param $reason * @param $suppress - * @throws MWException or FSException on database or filestore failure + * @throws MWException or FSException on database or file store failure * @return FileRepoStatus object. */ function deleteOld( $archiveName, $reason, $suppress=false ) { @@ -1386,7 +1393,7 @@ class LocalFileDeleteBatch { * Run the transaction */ function execute() { - global $wgUser, $wgUseSquid; + global $wgUseSquid; wfProfileIn( __METHOD__ ); $this->file->lock(); @@ -1399,7 +1406,7 @@ class LocalFileDeleteBatch { array( 'oi_archive_name' ), array( 'oi_name' => $this->file->getName(), 'oi_archive_name IN (' . $dbw->makeList( array_keys($oldRels) ) . ')', - 'oi_deleted & ' . File::DELETED_FILE => File::DELETED_FILE ), + $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ), __METHOD__ ); while( $row = $dbw->fetchObject( $res ) ) { $privateFiles[$row->oi_archive_name] = 1; @@ -1413,7 +1420,7 @@ class LocalFileDeleteBatch { foreach ( $this->srcRels as $name => $srcRel ) { // Skip files that have no hash (missing source). // Keep private files where they are. - if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) { + if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) { $hash = $hashes[$name]; $key = $hash . $dotExt; $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; @@ -1429,6 +1436,9 @@ class LocalFileDeleteBatch { // them in a separate transaction, then run the file ops, then update the fa_name fields. $this->doDBInserts(); + // Removes non-existent file from the batch, so we don't get errors. + $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch ); + // Execute the file deletion batch $status = $this->file->repo->deleteBatch( $this->deletionBatch ); if ( !$status->isGood() ) { @@ -1440,6 +1450,7 @@ class LocalFileDeleteBatch { // Roll back inserts, release lock and abort // TODO: delete the defunct filearchive rows if we are using a non-transactional DB $this->file->unlockAndRollback(); + wfProfileOut( __METHOD__ ); return $this->status; } @@ -1461,6 +1472,22 @@ class LocalFileDeleteBatch { wfProfileOut( __METHOD__ ); return $this->status; } + + /** + * Removes non-existent files from a deletion batch. + */ + function removeNonexistentFiles( $batch ) { + $files = $newBatch = array(); + foreach( $batch as $batchItem ) { + list( $src, $dest ) = $batchItem; + $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src ); + } + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); + foreach( $batch as $batchItem ) + if( $result[$batchItem[0]] ) + $newBatch[] = $batchItem; + return $newBatch; + } } #------------------------------------------------------------------------------ @@ -1508,7 +1535,7 @@ class LocalFileRestoreBatch { * So we save the batch and let the caller call cleanup() */ function execute() { - global $wgUser, $wgLang; + global $wgLang; if ( !$this->all && !$this->ids ) { // Do nothing return $this->file->repo->newGood(); @@ -1653,6 +1680,9 @@ class LocalFileRestoreBatch { $status->error( 'undelete-missing-filearchive', $id ); } + // Remove missing files from batch, so we don't get errors when undeleting them + $storeBatch = $this->removeNonexistentFiles( $storeBatch ); + // Run the store batch // Use the OVERWRITE_SAME flag to smooth over a common error $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); @@ -1683,9 +1713,10 @@ class LocalFileRestoreBatch { __METHOD__ ); } - if( $status->successCount > 0 ) { + // If store batch is empty (all files are missing), deletion is to be considered successful + if( $status->successCount > 0 || !$storeBatch ) { if( !$exists ) { - wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" ); + wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" ); // Update site_stats $site_stats = $dbw->tableName( 'site_stats' ); @@ -1693,7 +1724,7 @@ class LocalFileRestoreBatch { $this->file->purgeEverything(); } else { - wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" ); + wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" ); $this->file->purgeDescription(); $this->file->purgeHistory(); } @@ -1702,6 +1733,38 @@ class LocalFileRestoreBatch { return $status; } + /** + * Removes non-existent files from a store batch. + */ + function removeNonexistentFiles( $triplets ) { + $files = $filteredTriplets = array(); + foreach( $triplets as $file ) + $files[$file[0]] = $file[0]; + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); + foreach( $triplets as $file ) + if( $result[$file[0]] ) + $filteredTriplets[] = $file; + return $filteredTriplets; + } + + /** + * Removes non-existent files from a cleanup batch. + */ + function removeNonexistentFromCleanup( $batch ) { + $files = $newBatch = array(); + $repo = $this->file->repo; + foreach( $batch as $file ) { + $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' . + rawurlencode( $repo->getDeletedHashPath( $file ) . $file ); + } + + $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); + foreach( $batch as $file ) + if( $result[$file] ) + $newBatch[] = $file; + return $newBatch; + } + /** * Delete unused files in the deleted zone. * This should be called from outside the transaction in which execute() was called. @@ -1710,6 +1773,7 @@ class LocalFileRestoreBatch { if ( !$this->cleanupBatch ) { return $this->file->repo->newGood(); } + $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch ); $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch ); return $status; } @@ -1728,7 +1792,7 @@ class LocalFileMoveBatch { $this->file = $file; $this->target = $target; $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() ); - $this->newHash = $this->file->repo->getHashPath( $this->target->getDBKey() ); + $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() ); $this->oldName = $this->file->getName(); $this->newName = $this->file->repo->getNameFromTitle( $this->target ); $this->oldRel = $this->oldHash . $this->oldName; @@ -1736,14 +1800,14 @@ class LocalFileMoveBatch { $this->db = $file->repo->getMasterDb(); } - /* + /** * Add the current image to the batch */ function addCurrent() { $this->cur = array( $this->oldRel, $this->newRel ); } - /* + /** * Add the old versions of the image to the batch */ function addOlds() { @@ -1781,7 +1845,7 @@ class LocalFileMoveBatch { $this->db->freeResult( $result ); } - /* + /** * Perform the move. */ function execute() { @@ -1789,6 +1853,7 @@ class LocalFileMoveBatch { $status = $repo->newGood(); $triplets = $this->getMoveTriplets(); + $triplets = $this->removeNonexistentFiles( $triplets ); $statusDb = $this->doDBUpdates(); wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE ); @@ -1797,12 +1862,13 @@ class LocalFileMoveBatch { wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); $this->db->rollback(); } + $status->merge( $statusDb ); $status->merge( $statusMove ); return $status; } - /* + /** * Do the database updates and return a new WikiError indicating how many * rows where updated. */ @@ -1842,7 +1908,7 @@ class LocalFileMoveBatch { return $status; } - /* + /** * Generate triplets for FSRepo::storeBatch(). */ function getMoveTriplets() { @@ -1856,4 +1922,22 @@ class LocalFileMoveBatch { } return $triplets; } + + /** + * Removes non-existent files from move batch. + */ + function removeNonexistentFiles( $triplets ) { + $files = array(); + foreach( $triplets as $file ) + $files[$file[0]] = $file[0]; + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); + $filteredTriplets = array(); + foreach( $triplets as $file ) + if( $result[$file[0]] ) { + $filteredTriplets[] = $file; + } else { + wfDebugLog( 'imagemove', "File {$file[0]} does not exist" ); + } + return $filteredTriplets; + } } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index c679dd98..6c4d21a2 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -49,8 +49,8 @@ class LocalRepo extends FSRepo { $ext = File::normalizeExtension($ext); $inuse = $dbw->selectField( 'oldimage', '1', array( 'oi_sha1' => $sha1, - "oi_archive_name LIKE '%.{$ext}'", - 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ), + 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ), + $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ), __METHOD__, array( 'FOR UPDATE' ) ); } if ( !$inuse ) { @@ -83,17 +83,24 @@ class LocalRepo extends FSRepo { $title = Title::makeTitle( NS_FILE, $title->getText() ); } - $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) ); + $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) ); + if ( $memcKey === false ) { + $memcKey = $this->getLocalCacheKey( 'image_redirect', md5( $title->getDBkey() ) ); + $expiry = 300; // no invalidation, 5 minutes + } else { + $expiry = 86400; // has invalidation, 1 day + } $cachedValue = $wgMemc->get( $memcKey ); - if( $cachedValue ) { - return Title::newFromDbKey( $cachedValue ); - } elseif( $cachedValue == ' ' ) { # FIXME: ugly hack, but BagOStuff caching seems to be weird and return false if !cachedValue, not only if it doesn't exist + if ( $cachedValue === ' ' || $cachedValue === '' ) { + // Does not exist return false; - } + } elseif ( strval( $cachedValue ) !== '' ) { + return Title::newFromText( $cachedValue, NS_FILE ); + } // else $cachedValue is false or null: cache miss $id = $this->getArticleID( $title ); if( !$id ) { - $wgMemc->set( $memcKey, " ", 9000 ); + $wgMemc->set( $memcKey, " ", $expiry ); return false; } $dbr = $this->getSlaveDB(); @@ -104,12 +111,14 @@ class LocalRepo extends FSRepo { __METHOD__ ); - if( $row ) $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title ); - $wgMemc->set( $memcKey, ($row ? $targetTitle->getPrefixedDBkey() : " "), 9000 ); - if( !$row ) { + if( $row && $row->rd_namespace == NS_FILE ) { + $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title ); + $wgMemc->set( $memcKey, $targetTitle->getDBkey(), $expiry ); + return $targetTitle; + } else { + $wgMemc->set( $memcKey, '', $expiry ); return false; } - return $targetTitle; } @@ -127,15 +136,17 @@ class LocalRepo extends FSRepo { 'page_id', //Field array( //Conditions 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBKey(), + 'page_title' => $title->getDBkey(), ), __METHOD__ //Function name ); return $id; } - - + /** + * Get an array or iterator of file objects for files that have a given + * SHA-1 content hash. + */ function findBySha1( $hash ) { $dbr = $this->getSlaveDB(); $res = $dbr->select( @@ -150,28 +161,42 @@ class LocalRepo extends FSRepo { $res->free(); return $result; } - - /* - * Find many files using one query + + /** + * Get a connection to the slave DB */ - function findFiles( $titles ) { - // FIXME: Only accepts a $titles array where the keys are the sanitized - // file names. - - if ( count( $titles ) == 0 ) return array(); - - $dbr = $this->getSlaveDB(); - $res = $dbr->select( - 'image', - LocalFile::selectFields(), - array( 'img_name' => array_keys( $titles ) ) - ); - - $result = array(); - while ( $row = $res->fetchObject() ) { - $result[$row->img_name] = $this->newFileFromRow( $row ); + function getSlaveDB() { + return wfGetDB( DB_SLAVE ); + } + + /** + * Get a connection to the master DB + */ + function getMasterDB() { + return wfGetDB( DB_MASTER ); + } + + /** + * Get a key on the primary cache for this repository. + * Returns false if the repository's cache is not accessible at this site. + * The parameters are the parts of the key, as for wfMemcKey(). + */ + function getSharedCacheKey( /*...*/ ) { + $args = func_get_args(); + return call_user_func_array( 'wfMemcKey', $args ); + } + + /** + * Invalidates image redirect cache related to that image + * + * @param Title $title Title of image + */ + function invalidateImageRedirect( $title ) { + global $wgMemc; + $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) ); + if ( $memcKey ) { + $wgMemc->delete( $memcKey ); } - $res->free(); - return $result; } } + diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index fb89cebb..2bc61bde 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -14,19 +14,25 @@ class NullRepo extends FileRepo { function storeTemp( $originalName, $srcPath ) { return false; } + function append( $srcPath, $toAppendPath, $flags = 0 ){ + return false; + } function publishBatch( $triplets, $flags = 0 ) { return false; } function deleteBatch( $sourceDestPairs ) { return false; } + function fileExistsBatch( $files, $flags = 0 ) { + return false; + } function getFileProps( $virtualUrl ) { return false; } function newFile( $title, $time = false ) { return false; } - function findFile( $title, $time = false ) { + function findFile( $title, $options = array() ) { return false; } } diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php index 46c35bd9..35f3f9f2 100644 --- a/includes/filerepo/OldLocalFile.php +++ b/includes/filerepo/OldLocalFile.php @@ -177,25 +177,27 @@ class OldLocalFile extends LocalFile { * @return bool */ function isDeleted( $field ) { + $this->load(); return ($this->deleted & $field) == $field; } + /** + * Returns bitfield value + * @return int + */ + function getVisibility() { + $this->load(); + return (int)$this->deleted; + } + /** * Determine if the current user is allowed to view a particular - * field of this FileStore image file, if it's marked as deleted. + * field of this image file, if it's marked as deleted. * @param int $field * @return bool */ function userCan( $field ) { - if( isset($this->deleted) && ($this->deleted & $field) == $field ) { - global $wgUser; - $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED - ? 'suppressrevision' - : 'deleterevision'; - wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); - return $wgUser->isAllowed( $permission ); - } else { - return true; - } + $this->load(); + return Revision::userCanBitfield( $this->deleted, $field ); } } diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 2303f581..1465400c 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -13,8 +13,10 @@ class RepoGroup { var $localRepo, $foreignRepos, $reposInitialised = false; var $localInfo, $foreignInfo; + var $cache; protected static $instance; + const MAX_CACHE_SIZE = 1000; /** * Get a RepoGroup instance. At present only one instance of RepoGroup is @@ -54,56 +56,116 @@ class RepoGroup { function __construct( $localInfo, $foreignInfo ) { $this->localInfo = $localInfo; $this->foreignInfo = $foreignInfo; + $this->cache = array(); } /** * Search repositories for an image. - * You can also use wfGetFile() to do this. + * You can also use wfFindFile() to do this. * @param mixed $title Title object or string - * @param mixed $time The 14-char timestamp the file should have - * been uploaded, or false for the current version - * @param mixed $flags FileRepo::FIND_ flags + * @param $options Associative array of options: + * time: requested time for an archived image, or false for the + * current version. An image object will be returned which was + * created at the specified time. + * + * ignoreRedirect: If true, do not follow file redirects + * + * private: If true, return restricted (deleted) files if the current + * user is allowed to view them. Otherwise, such files will not + * be found. + * + * bypassCache: If true, do not use the process-local cache of File objects * @return File object or false if it is not found */ - function findFile( $title, $time = false, $flags = 0 ) { + function findFile( $title, $options = array() ) { + if ( !is_array( $options ) ) { + // MW 1.15 compat + $options = array( 'time' => $options ); + } if ( !$this->reposInitialised ) { $this->initialiseRepos(); } + if ( !($title instanceof Title) ) { + $title = Title::makeTitleSafe( NS_FILE, $title ); + if ( !is_object( $title ) ) { + return false; + } + } - $image = $this->localRepo->findFile( $title, $time, $flags ); + # Check the cache + if ( empty( $options['ignoreRedirect'] ) + && empty( $options['private'] ) + && empty( $options['bypassCache'] ) ) + { + $useCache = true; + $time = isset( $options['time'] ) ? $options['time'] : ''; + $dbkey = $title->getDBkey(); + if ( isset( $this->cache[$dbkey][$time] ) ) { + wfDebug( __METHOD__.": got File:$dbkey from process cache\n" ); + # Move it to the end of the list so that we can delete the LRU entry later + $tmp = $this->cache[$dbkey]; + unset( $this->cache[$dbkey] ); + $this->cache[$dbkey] = $tmp; + # Return the entry + return $this->cache[$dbkey][$time]; + } else { + # Add a negative cache entry, may be overridden + $this->trimCache(); + $this->cache[$dbkey][$time] = false; + $cacheEntry =& $this->cache[$dbkey][$time]; + } + } else { + $useCache = false; + } + + # Check the local repo + $image = $this->localRepo->findFile( $title, $options ); if ( $image ) { + if ( $useCache ) { + $cacheEntry = $image; + } return $image; } + + # Check the foreign repos foreach ( $this->foreignRepos as $repo ) { - $image = $repo->findFile( $title, $time, $flags ); + $image = $repo->findFile( $title, $options ); if ( $image ) { + if ( $useCache ) { + $cacheEntry = $image; + } return $image; } } + # Not found, do not override negative cache return false; } - function findFiles( $titles ) { + + function findFiles( $inputItems ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } - $titleObjs = array(); - foreach ( $titles as $title ) { - if ( !( $title instanceof Title ) ) - $title = Title::makeTitleSafe( NS_FILE, $title ); - if ( $title ) - $titleObjs[$title->getDBkey()] = $title; + $items = array(); + foreach ( $inputItems as $item ) { + if ( !is_array( $item ) ) { + $item = array( 'title' => $item ); + } + if ( !( $item['title'] instanceof Title ) ) + $item['title'] = Title::makeTitleSafe( NS_FILE, $item['title'] ); + if ( $item['title'] ) + $items[$item['title']->getDBkey()] = $item; } - $images = $this->localRepo->findFiles( $titleObjs ); + $images = $this->localRepo->findFiles( $items ); foreach ( $this->foreignRepos as $repo ) { - // Remove found files from $titleObjs - foreach ( $images as $name => $image ) - if ( isset( $titleObjs[$name] ) ) - unset( $titleObjs[$name] ); - - $images = array_merge( $images, $repo->findFiles( $titleObjs ) ); + // Remove found files from $items + foreach ( $images as $name => $image ) { + unset( $items[$name] ); + } + + $images = array_merge( $images, $repo->findFiles( $items ) ); } return $images; } @@ -128,16 +190,16 @@ class RepoGroup { } return false; } - + function findBySha1( $hash ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } - + $result = $this->localRepo->findBySha1( $hash ); foreach ( $this->foreignRepos as $repo ) $result = array_merge( $result, $repo->findBySha1( $hash ) ); - return $result; + return $result; } /** @@ -178,7 +240,7 @@ class RepoGroup { } /** - * Call a function for each foreign repo, with the repo object as the + * Call a function for each foreign repo, with the repo object as the * first parameter. * * @param $callback callback The function to call @@ -254,4 +316,16 @@ class RepoGroup { return File::getPropsFromPath( $fileName ); } } + + /** + * Limit cache memory + */ + function trimCache() { + while ( count( $this->cache ) >= self::MAX_CACHE_SIZE ) { + reset( $this->cache ); + $key = key( $this->cache ); + wfDebug( __METHOD__.": evicting $key\n" ); + unset( $this->cache[$key] ); + } + } } -- cgit v1.2.3-54-g00ecf