From ed4303922f4e850418168595c2a22cdd895081cc Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Wed, 30 May 2007 21:02:32 +0000 Subject: [PATCH] Merged filerepo-work branch: * Added support for configuration of an arbitrary number of commons-style file repositories. * Split Image.php into filerepo/File.php and filerepo/LocalFile.php * Renamed Image::getImagePath() to File::getPath() * Added initial support for timestamp-based file fetching (OldLocalFile), to be expanded upon by aaron. * Changed the interface for Image/File object creation: use wfFindFile() or wfLocalFile() depending on semantics * ImageGallery::add() now accepts a title object as the first parameter * Moved file handling operations on upload from SpecialUpload to File * Removed path-related functions from ImageFunctions.php. Removed static path accessors from File. * Added a Content-Disposition header to thumb.php output * Improved thumb.php error handling * Updated the unit test suite to kind of partially work with modern computers. RunTests.php doesn't work just yet. Fixed an actual regression that the test suite detected -- moved some defines to Defines.php where they will be loaded consistently. --- RELEASE-NOTES | 5 + StartProfiler.php | 10 +- includes/AutoLoader.php | 15 +- includes/CategoryPage.php | 4 +- includes/DefaultSettings.php | 47 + includes/Defines.php | 56 + includes/ExternalEdit.php | 2 +- includes/GlobalFunctions.php | 22 + includes/Image.php | 2154 ----------------- includes/ImageFunctions.php | 109 - includes/ImageGallery.php | 26 +- includes/ImagePage.php | 121 +- includes/ImageQueryPage.php | 6 +- includes/Linker.php | 94 +- includes/MediaTransformOutput.php | 2 +- includes/Parser.php | 6 +- includes/SearchEngine.php | 4 +- includes/Setup.php | 53 + includes/Skin.php | 4 +- includes/SpecialImagelist.php | 4 +- includes/SpecialMIMEsearch.php | 2 +- includes/SpecialNewimages.php | 3 +- includes/SpecialUndelete.php | 2 +- includes/SpecialUpload.php | 98 +- includes/StreamFile.php | 3 + includes/filerepo/ArchivedFile.php | 112 + includes/filerepo/FSRepo.php | 368 +++ includes/filerepo/File.php | 985 ++++++++ includes/filerepo/ForeignDBFile.php | 39 + includes/filerepo/ForeignDBRepo.php | 51 + includes/filerepo/LocalFile.php | 1331 ++++++++++ includes/filerepo/LocalRepo.php | 26 + includes/filerepo/OldLocalFile.php | 222 ++ includes/filerepo/RepoGroup.php | 98 + includes/filerepo/UnregisteredLocalFile.php | 109 + includes/media/Bitmap.php | 7 +- includes/media/DjVu.php | 2 +- includes/media/Generic.php | 6 +- includes/media/SVG.php | 4 +- includes/normal/UtfNormal.php | 53 - maintenance/FiveUpgrade.inc | 54 +- maintenance/cleanupImages.php | 5 +- maintenance/importImages.inc.php | 18 +- maintenance/importImages.php | 62 +- maintenance/rebuildImages.php | 141 +- tests/ArticleTest.php | 44 +- tests/DatabaseTest.php | 17 +- tests/GlobalTest.php | 28 +- .../{ImageTest.php => ImageFunctionsTest.php} | 21 +- tests/README | 10 +- tests/RunTests.php | 32 +- tests/SanitizerTest.php | 18 +- tests/SearchEngineTest.php | 15 +- tests/SearchMySQL4Test.php | 2 - tests/run-test.php | 7 + thumb.php | 59 +- 56 files changed, 3867 insertions(+), 2931 deletions(-) delete mode 100644 includes/Image.php create mode 100644 includes/filerepo/ArchivedFile.php create mode 100644 includes/filerepo/FSRepo.php create mode 100644 includes/filerepo/File.php create mode 100644 includes/filerepo/ForeignDBFile.php create mode 100644 includes/filerepo/ForeignDBRepo.php create mode 100644 includes/filerepo/LocalFile.php create mode 100644 includes/filerepo/LocalRepo.php create mode 100644 includes/filerepo/OldLocalFile.php create mode 100644 includes/filerepo/RepoGroup.php create mode 100644 includes/filerepo/UnregisteredLocalFile.php rename tests/{ImageTest.php => ImageFunctionsTest.php} (67%) create mode 100644 tests/run-test.php diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 309ddd5f8a..49e4204f7e 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -43,6 +43,11 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN * Introducing 'frameless' keyword to [[Image:]] syntax which respects the user preferences for image width like 'thumb' but without a frame. * (bug 7960) Link to "what links here" for each "what links here" entry +* Added support for configuration of an arbitrary number of commons-style + file repositories. +* Added a Content-Disposition header to thumb.php output +* Improved thumb.php error handling + == Bugfixes since 1.10 == diff --git a/StartProfiler.php b/StartProfiler.php index 8fc3ff887f..a256449545 100644 --- a/StartProfiler.php +++ b/StartProfiler.php @@ -1,22 +1,24 @@ diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index ca198ebf33..09426d196f 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -96,8 +96,6 @@ function __autoload($className) { 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', 'EnotifNotifyJob' => 'includes/JobQueue.php', 'Http' => 'includes/HttpFunctions.php', - 'Image' => 'includes/Image.php', - 'ArchivedFile' => 'includes/Image.php', 'IP' => 'includes/IP.php', 'ThumbnailImage' => 'includes/Image.php', 'ImageGallery' => 'includes/ImageGallery.php', @@ -250,6 +248,19 @@ function __autoload($className) { 'memcached' => 'includes/memcached-client.php', 'EmaillingJob' => 'includes/JobQueue.php', + # filerepo + 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', + 'File' => 'includes/filerepo/File.php', + 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', + 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', + 'FSRepo' => 'includes/filerepo/FSRepo.php', + 'Image' => 'includes/filerepo/LocalFile.php', + 'LocalFile' => 'includes/filerepo/LocalFile.php', + 'LocalRepo' => 'includes/filerepo/LocalRepo.php', + 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', + 'RepoGroup' => 'includes/filerepo/RepoGroup.php', + 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', + # Media 'BitmapHandler' => 'includes/media/Bitmap.php', 'BmpHandler' => 'includes/media/BMP.php', diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 48f545d225..2c9a1636a9 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -147,7 +147,7 @@ class CategoryViewer { /** * Add a page in the image namespace */ - function addImage( $title, $sortkey, $pageLength, $isRedirect = false ) { + function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { if ( $this->showGallery ) { $image = new Image( $title ); if( $this->flip ) { @@ -222,7 +222,7 @@ class CategoryViewer { if( $title->getNamespace() == NS_CATEGORY ) { $this->addSubcategory( $title, $x->cl_sortkey, $x->page_len ); - } elseif( $title->getNamespace() == NS_IMAGE ) { + } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) { $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } else { $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c2fe7b5ea9..f0bbe3e6bc 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -184,6 +184,49 @@ $wgFileStore['deleted']['directory'] = null; // Don't forget to set this. $wgFileStore['deleted']['url'] = null; // Private $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split +/**#@+ + * File repository structures + * + * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is + * a an array of such structures. Each repository structure is an associative + * array of properties configuring the repository. + * + * Properties required for all repos: + * class The class name for the repository. May come from the core or an extension. + * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo. + * + * name A unique name for the repository. + * + * For all core repos: + * url Base public URL + * hashLevels The number of directory levels for hash-based division of files + * thumbScriptUrl The URL for thumb.php (optional, not recommended) + * transformVia404 Whether to skip media file transformation on parse and rely on a 404 + * handler instead. + * + * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored + * for local repositories: + * descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/Image: + * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. + * http://en.wikipedia.org/w + * + * articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 + * fetchDescription Fetch the text of the remote file description page. Equivalent to + * $wgFetchCommonsDescriptions. + * + * ForeignDBRepo: + * dbType, dbServer, dbUser, dbPassword, dbName, dbFlags + * equivalent to the corresponding member of $wgDBservers + * tablePrefix Table prefix, the foreign wiki's $wgDBprefix + * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc + * + * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: + * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. + */ +$wgLocalFileRepo = false; +$wgForeignFileRepos = array(); +/**#@-*/ + /** * Allowed title characters -- regex character class * Don't change this unless you know what you're doing @@ -355,6 +398,10 @@ $wgActionPaths = array(); * no file of the given name is found in the local repository (for [[Image:..]], * [[Media:..]] links). Thumbnails will also be looked for and generated in this * directory. + * + * Note that these configuration settings can now be defined on a per- + * repository basis for an arbitrary number of file repositories, using the + * $wgForeignFileRepos variable. */ $wgUseSharedUploads = false; /** Full path on the web server where shared uploads can be found */ diff --git a/includes/Defines.php b/includes/Defines.php index 98e76277fb..5705c388b9 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -205,5 +205,61 @@ define( 'LIST_SET', 2 ); define( 'LIST_NAMES', 3); define( 'LIST_OR', 4); +/** + * Unicode and normalisation related + */ +define( 'UNICODE_HANGUL_FIRST', 0xac00 ); +define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); + +define( 'UNICODE_HANGUL_LBASE', 0x1100 ); +define( 'UNICODE_HANGUL_VBASE', 0x1161 ); +define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); + +define( 'UNICODE_HANGUL_LCOUNT', 19 ); +define( 'UNICODE_HANGUL_VCOUNT', 21 ); +define( 'UNICODE_HANGUL_TCOUNT', 28 ); +define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); + +define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); +define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); +define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); + +define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); +define( 'UNICODE_SURROGATE_LAST', 0xdfff ); +define( 'UNICODE_MAX', 0x10ffff ); +define( 'UNICODE_REPLACEMENT', 0xfffd ); + + +define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); +define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); + +define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); +define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); +define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); + +define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); +define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); +define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); + +define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); +define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); +define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); +define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); +#define( 'UTF8_REPLACEMENT', '!' ); + +define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); +define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); +define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); + +# These two ranges are illegal +define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); +define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); +define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); +define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); + +define( 'UTF8_HEAD', false ); +define( 'UTF8_TAIL', true ); + + ?> diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index c8ed8bdee4..4d9f1e2117 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -46,7 +46,7 @@ class ExternalEdit { $extension="wiki"; } elseif($this->mMode=="file") { $type="Edit file"; - $image = new Image( $this->mTitle ); + $image = wfLocalFile( $this->mTitle ); $img_url = $image->getURL(); if(strpos($img_url,"://")) { $url = $img_url; diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 88e1fe5aed..6ab81e509e 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2272,4 +2272,26 @@ function &wfGetDB( $db = DB_LAST, $groups = array() ) { $ret = $wgLoadBalancer->getConnection( $db, true, $groups ); return $ret; } + +/** + * Find a file. + * Shortcut for RepoGroup::singleton()->findFile() + * @param mixed $title Title object or string. May be interwiki. + * @param mixed $time Requested time for an archived image, or false for the + * current version. An image object will be returned which + * existed at or before the specified time. + * @return File, or false if the file does not exist + */ +function wfFindFile( $title, $time = false ) { + return RepoGroup::singleton()->findFile( $title, $time ); +} + +/** + * Get an object referring to a locally registered file. + * Returns a valid placeholder object if the file does not exist. + */ +function wfLocalFile( $title ) { + return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); +} + ?> diff --git a/includes/Image.php b/includes/Image.php deleted file mode 100644 index adcee1432d..0000000000 --- a/includes/Image.php +++ /dev/null @@ -1,2154 +0,0 @@ -title =& $title; - $this->name = $title->getDBkey(); - $this->metadata = ''; - - $n = strrpos( $this->name, '.' ); - $this->extension = Image::normalizeExtension( $n ? - substr( $this->name, $n + 1 ) : '' ); - $this->historyLine = 0; - - $this->dataLoaded = false; - } - - /** - * Normalize a file extension to the common form, and ensure it's clean. - * Extensions with non-alphanumeric characters will be discarded. - * - * @param string $ext (without the .) - * @return string - */ - static function normalizeExtension( $ext ) { - $lower = strtolower( $ext ); - $squish = array( - 'htm' => 'html', - 'jpeg' => 'jpg', - 'mpeg' => 'mpg', - 'tiff' => 'tif' ); - if( isset( $squish[$lower] ) ) { - return $squish[$lower]; - } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) { - return $lower; - } else { - return ''; - } - } - - /** - * Get the memcached keys - * @return array[int]mixed Returns an array, first element is the local cache key, second is the shared cache key, if there is one - */ - function getCacheKeys( ) { - global $wgUseSharedUploads, $wgSharedUploadDBname, $wgCacheSharedUploads; - - $hashedName = md5($this->name); - $keys = array( wfMemcKey( 'Image', $hashedName ) ); - if ( $wgUseSharedUploads && $wgSharedUploadDBname && $wgCacheSharedUploads ) { - $keys[] = wfForeignMemcKey( $wgSharedUploadDBname, false, 'Image', $hashedName ); - } - return $keys; - } - - /** - * Try to load image metadata from memcached. Returns true on success. - */ - function loadFromCache() { - global $wgUseSharedUploads, $wgMemc; - wfProfileIn( __METHOD__ ); - $this->dataLoaded = false; - $keys = $this->getCacheKeys(); - $cachedValues = $wgMemc->get( $keys[0] ); - - // Check if the key existed and belongs to this version of MediaWiki - if (!empty($cachedValues) && is_array($cachedValues) - && isset($cachedValues['version']) && ( $cachedValues['version'] == MW_IMAGE_VERSION ) - && isset( $cachedValues['mime'] ) && isset( $cachedValues['metadata'] ) ) - { - if ( $wgUseSharedUploads && $cachedValues['fromShared']) { - # if this is shared file, we need to check if image - # in shared repository has not changed - if ( isset( $keys[1] ) ) { - $commonsCachedValues = $wgMemc->get( $keys[1] ); - if (!empty($commonsCachedValues) && is_array($commonsCachedValues) - && isset($commonsCachedValues['version']) - && ( $commonsCachedValues['version'] == MW_IMAGE_VERSION ) - && isset($commonsCachedValues['mime'])) { - wfDebug( "Pulling image metadata from shared repository cache\n" ); - $this->name = $commonsCachedValues['name']; - $this->imagePath = $commonsCachedValues['imagePath']; - $this->fileExists = $commonsCachedValues['fileExists']; - $this->width = $commonsCachedValues['width']; - $this->height = $commonsCachedValues['height']; - $this->bits = $commonsCachedValues['bits']; - $this->type = $commonsCachedValues['type']; - $this->mime = $commonsCachedValues['mime']; - $this->metadata = $commonsCachedValues['metadata']; - $this->size = $commonsCachedValues['size']; - $this->fromSharedDirectory = true; - $this->dataLoaded = true; - $this->imagePath = $this->getFullPath(true); - } - } - } else { - wfDebug( "Pulling image metadata from local cache\n" ); - $this->name = $cachedValues['name']; - $this->imagePath = $cachedValues['imagePath']; - $this->fileExists = $cachedValues['fileExists']; - $this->width = $cachedValues['width']; - $this->height = $cachedValues['height']; - $this->bits = $cachedValues['bits']; - $this->type = $cachedValues['type']; - $this->mime = $cachedValues['mime']; - $this->metadata = $cachedValues['metadata']; - $this->size = $cachedValues['size']; - $this->fromSharedDirectory = false; - $this->dataLoaded = true; - $this->imagePath = $this->getFullPath(); - } - } - if ( $this->dataLoaded ) { - wfIncrStats( 'image_cache_hit' ); - } else { - wfIncrStats( 'image_cache_miss' ); - } - - wfProfileOut( __METHOD__ ); - return $this->dataLoaded; - } - - /** - * Save the image metadata to memcached - */ - function saveToCache() { - global $wgMemc, $wgUseSharedUploads; - $this->load(); - $keys = $this->getCacheKeys(); - // We can't cache negative metadata for non-existent files, - // because if the file later appears in commons, the local - // keys won't be purged. - if ( $this->fileExists || !$wgUseSharedUploads ) { - $cachedValues = array( - 'version' => MW_IMAGE_VERSION, - 'name' => $this->name, - 'imagePath' => $this->imagePath, - 'fileExists' => $this->fileExists, - 'fromShared' => $this->fromSharedDirectory, - 'width' => $this->width, - 'height' => $this->height, - 'bits' => $this->bits, - 'type' => $this->type, - 'mime' => $this->mime, - 'metadata' => $this->metadata, - 'size' => $this->size ); - - $wgMemc->set( $keys[0], $cachedValues, 60 * 60 * 24 * 7 ); // A week - } else { - // However we should clear them, so they aren't leftover - // if we've deleted the file. - $wgMemc->delete( $keys[0] ); - } - } - - /** - * Load metadata from the file itself - */ - function loadFromFile() { - global $wgUseSharedUploads, $wgSharedUploadDirectory, $wgContLang; - wfProfileIn( __METHOD__ ); - $this->imagePath = $this->getFullPath(); - $this->fileExists = file_exists( $this->imagePath ); - $this->fromSharedDirectory = false; - $gis = array(); - - if (!$this->fileExists) wfDebug(__METHOD__.': '.$this->imagePath." not found locally!\n"); - - # If the file is not found, and a shared upload directory is used, look for it there. - if (!$this->fileExists && $wgUseSharedUploads && $wgSharedUploadDirectory) { - # In case we're on a wgCapitalLinks=false wiki, we - # capitalize the first letter of the filename before - # looking it up in the shared repository. - $sharedImage = Image::newFromName( $wgContLang->ucfirst($this->name) ); - $this->fileExists = $sharedImage && file_exists( $sharedImage->getFullPath(true) ); - if ( $this->fileExists ) { - $this->name = $sharedImage->name; - $this->imagePath = $this->getFullPath(true); - $this->fromSharedDirectory = true; - } - } - - - if ( $this->fileExists ) { - $magic=& MimeMagic::singleton(); - - $this->mime = $magic->guessMimeType($this->imagePath,true); - $this->type = $magic->getMediaType($this->imagePath,$this->mime); - $handler = MediaHandler::getHandler( $this->mime ); - - # Get size in bytes - $this->size = filesize( $this->imagePath ); - - # Height, width and metadata - if ( $handler ) { - $gis = $handler->getImageSize( $this, $this->imagePath ); - $this->metadata = $handler->getMetadata( $this, $this->imagePath ); - } else { - $gis = false; - $this->metadata = ''; - } - - wfDebug(__METHOD__.': '.$this->imagePath." loaded, ".$this->size." bytes, ".$this->mime.".\n"); - } - else { - $this->mime = NULL; - $this->type = MEDIATYPE_UNKNOWN; - $this->metadata = ''; - wfDebug(__METHOD__.': '.$this->imagePath." NOT FOUND!\n"); - } - - if( $gis ) { - $this->width = $gis[0]; - $this->height = $gis[1]; - } else { - $this->width = 0; - $this->height = 0; - } - - #NOTE: $gis[2] contains a code for the image type. This is no longer used. - - #NOTE: we have to set this flag early to avoid load() to be called - # be some of the functions below. This may lead to recursion or other bad things! - # as ther's only one thread of execution, this should be safe anyway. - $this->dataLoaded = true; - - if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; - else $this->bits = 0; - - wfProfileOut( __METHOD__ ); - } - - /** - * Load image metadata from the DB - */ - function loadFromDB() { - global $wgUseSharedUploads, $wgSharedUploadDBname, $wgSharedUploadDBprefix, $wgContLang; - wfProfileIn( __METHOD__ ); - - $dbr = wfGetDB( DB_SLAVE ); - - $row = $dbr->selectRow( 'image', - array( 'img_size', 'img_width', 'img_height', 'img_bits', - 'img_media_type', 'img_major_mime', 'img_minor_mime', 'img_metadata' ), - array( 'img_name' => $this->name ), __METHOD__ ); - if ( $row ) { - $this->fromSharedDirectory = false; - $this->fileExists = true; - $this->loadFromRow( $row ); - $this->imagePath = $this->getFullPath(); - // Check for rows from a previous schema, quietly upgrade them - $this->maybeUpgradeRow(); - } elseif ( $wgUseSharedUploads && $wgSharedUploadDBname ) { - # In case we're on a wgCapitalLinks=false wiki, we - # capitalize the first letter of the filename before - # looking it up in the shared repository. - $name = $wgContLang->ucfirst($this->name); - $dbc = Image::getCommonsDB(); - - $row = $dbc->selectRow( "`$wgSharedUploadDBname`.{$wgSharedUploadDBprefix}image", - array( - 'img_size', 'img_width', 'img_height', 'img_bits', - 'img_media_type', 'img_major_mime', 'img_minor_mime', 'img_metadata' ), - array( 'img_name' => $name ), __METHOD__ ); - if ( $row ) { - $this->fromSharedDirectory = true; - $this->fileExists = true; - $this->imagePath = $this->getFullPath(true); - $this->name = $name; - $this->loadFromRow( $row ); - - // Check for rows from a previous schema, quietly upgrade them - $this->maybeUpgradeRow(); - } - } - - if ( !$row ) { - $this->size = 0; - $this->width = 0; - $this->height = 0; - $this->bits = 0; - $this->type = 0; - $this->fileExists = false; - $this->fromSharedDirectory = false; - $this->metadata = ''; - $this->mime = false; - } - - # Unconditionally set loaded=true, we don't want the accessors constantly rechecking - $this->dataLoaded = true; - wfProfileOut( __METHOD__ ); - } - - /* - * Load image metadata from a DB result row - */ - function loadFromRow( &$row ) { - $this->size = $row->img_size; - $this->width = $row->img_width; - $this->height = $row->img_height; - $this->bits = $row->img_bits; - $this->type = $row->img_media_type; - - $major= $row->img_major_mime; - $minor= $row->img_minor_mime; - - if (!$major) $this->mime = "unknown/unknown"; - else { - if (!$minor) $minor= "unknown"; - $this->mime = $major.'/'.$minor; - } - $this->metadata = $row->img_metadata; - - $this->dataLoaded = true; - } - - /** - * Load image metadata from cache or DB, unless already loaded - */ - function load() { - global $wgSharedUploadDBname, $wgUseSharedUploads; - if ( !$this->dataLoaded ) { - if ( !$this->loadFromCache() ) { - $this->loadFromDB(); - if ( !$wgSharedUploadDBname && $wgUseSharedUploads ) { - $this->loadFromFile(); - } elseif ( $this->fileExists || !$wgUseSharedUploads ) { - // We can do negative caching for local images, because the cache - // will be purged on upload. But we can't do it when shared images - // are enabled, since updates to that won't purge foreign caches. - $this->saveToCache(); - } - } - $this->dataLoaded = true; - } - } - - /** - * Upgrade a row if it needs it - * @return void - */ - function maybeUpgradeRow() { - if ( is_null($this->type) || $this->mime == 'image/svg' ) { - $this->upgradeRow(); - } else { - $handler = $this->getHandler(); - if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { - $this->upgradeRow(); - } - } - } - - /** - * Fix assorted version-related problems with the image row by reloading it from the file - */ - function upgradeRow() { - global $wgDBname, $wgSharedUploadDBname; - wfProfileIn( __METHOD__ ); - - $this->loadFromFile(); - - if ( $this->fromSharedDirectory ) { - if ( !$wgSharedUploadDBname ) { - wfProfileOut( __METHOD__ ); - return; - } - - // Write to the other DB using selectDB, not database selectors - // This avoids breaking replication in MySQL - $dbw = Image::getCommonsDB(); - } else { - $dbw = wfGetDB( DB_MASTER ); - } - - list( $major, $minor ) = self::splitMime( $this->mime ); - - wfDebug(__METHOD__.': upgrading '.$this->name." to the current schema\n"); - - $dbw->update( 'image', - array( - 'img_width' => $this->width, - 'img_height' => $this->height, - 'img_bits' => $this->bits, - 'img_media_type' => $this->type, - 'img_major_mime' => $major, - 'img_minor_mime' => $minor, - 'img_metadata' => $this->metadata, - ), array( 'img_name' => $this->name ), __METHOD__ - ); - if ( $this->fromSharedDirectory ) { - $dbw->selectDB( $wgDBname ); - } - wfProfileOut( __METHOD__ ); - } - - /** - * Split an internet media type into its two components; if not - * a two-part name, set the minor type to 'unknown'. - * - * @param string $mime "text/html" etc - * @return array ("text", "html") etc - */ - static function splitMime( $mime ) { - if( strpos( $mime, '/' ) !== false ) { - return explode( '/', $mime, 2 ); - } else { - return array( $mime, 'unknown' ); - } - } - - /** - * Return the name of this image - * @public - */ - function getName() { - return $this->name; - } - - /** - * Return the associated title object - * @public - */ - function getTitle() { - return $this->title; - } - - /** - * Return the URL of the image file - * @public - */ - function getURL() { - if ( !$this->url ) { - $this->load(); - if($this->fileExists) { - $this->url = Image::imageUrl( $this->name, $this->fromSharedDirectory ); - } else { - $this->url = ''; - } - } - return $this->url; - } - - function getViewURL() { - if( $this->mustRender()) { - if( $this->canRender() ) { - return $this->createThumb( $this->getWidth() ); - } - else { - wfDebug('Image::getViewURL(): supposed to render '.$this->name.' ('.$this->mime."), but can't!\n"); - return $this->getURL(); #hm... return NULL? - } - } else { - return $this->getURL(); - } - } - - /** - * Return the image path of the image in the - * local file system as an absolute path - * @public - */ - function getImagePath() { - $this->load(); - return $this->imagePath; - } - - /** - * @return mixed Return the width of the image; returns false on error. - * @param int $page Page number to find the width of. - * @public - */ - function getWidth( $page = 1 ) { - $this->load(); - if ( $this->isMultipage() ) { - $dim = $this->getHandler()->getPageDimensions( $this, $page ); - if ( $dim ) { - return $dim['width']; - } else { - return false; - } - } else { - return $this->width; - } - } - - /** - * @return mixed Return the height of the image; Returns false on error. - * @param int $page Page number to find the height of. - * @public - */ - function getHeight( $page = 1 ) { - $this->load(); - if ( $this->isMultipage() ) { - $dim = $this->getHandler()->getPageDimensions( $this, $page ); - if ( $dim ) { - return $dim['height']; - } else { - return false; - } - } else { - return $this->height; - } - } - - /** - * Get handler-specific metadata - */ - function getMetadata() { - $this->load(); - return $this->metadata; - } - - /** - * @return int the size of the image file, in bytes - * @public - */ - function getSize() { - $this->load(); - return $this->size; - } - - /** - * @return string the mime type of the file. - */ - function getMimeType() { - $this->load(); - return $this->mime; - } - - /** - * Return the type of the media in the file. - * Use the value returned by this function with the MEDIATYPE_xxx constants. - */ - function getMediaType() { - $this->load(); - return $this->type; - } - - /** - * Checks if the file can be presented to the browser as a bitmap. - * - * Currently, this checks if the file is an image format - * that can be converted to a format - * supported by all browsers (namely GIF, PNG and JPEG), - * or if it is an SVG image and SVG conversion is enabled. - * - * @todo remember the result of this check. - * @return boolean - */ - function canRender() { - $handler = $this->getHandler(); - return $handler && $handler->canRender(); - } - - /** - * Return true if the file is of a type that can't be directly - * rendered by typical browsers and needs to be re-rasterized. - * - * This returns true for everything but the bitmap types - * supported by all browsers, i.e. JPEG; GIF and PNG. It will - * also return true for any non-image formats. - * - * @return bool - */ - function mustRender() { - $handler = $this->getHandler(); - return $handler && $handler->mustRender(); - } - - /** - * Determines if this media file may be shown inline on a page. - * - * This is currently synonymous to canRender(), but this could be - * extended to also allow inline display of other media, - * like flash animations or videos. If you do so, please keep in mind that - * that could be a security risk. - */ - function allowInlineDisplay() { - return $this->canRender(); - } - - /** - * Determines if this media file is in a format that is unlikely to - * contain viruses or malicious content. It uses the global - * $wgTrustedMediaFormats list to determine if the file is safe. - * - * This is used to show a warning on the description page of non-safe files. - * It may also be used to disallow direct [[media:...]] links to such files. - * - * Note that this function will always return true if allowInlineDisplay() - * or isTrustedFile() is true for this file. - * - * @return boolean - */ - function isSafeFile() { - if ($this->allowInlineDisplay()) return true; - if ($this->isTrustedFile()) return true; - - global $wgTrustedMediaFormats; - - $type= $this->getMediaType(); - $mime= $this->getMimeType(); - #wfDebug("Image::isSafeFile: type= $type, mime= $mime\n"); - - if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted - if ( in_array( $type, $wgTrustedMediaFormats) ) return true; - - if ($mime==="unknown/unknown") return false; #unknown type, not trusted - if ( in_array( $mime, $wgTrustedMediaFormats) ) return true; - - return false; - } - - /** - * Returns true if the file is flagged as trusted. Files flagged that way - * can be linked to directly, even if that is not allowed for this type of - * file normally. - * - * This is a dummy function right now and always returns false. It could be - * implemented to extract a flag from the database. The trusted flag could be - * set on upload, if the user has sufficient privileges, to bypass script- - * and html-filters. It may even be coupled with cryptographics signatures - * or such. - * @return boolean - */ - function isTrustedFile() { - #this could be implemented to check a flag in the database, - #look for signatures, etc - return false; - } - - /** - * Return the escapeLocalURL of this image - * @param string $query URL query string - * @public - */ - function getEscapeLocalURL( $query=false) { - return $this->getTitle()->escapeLocalURL( $query ); - } - - /** - * Return the escapeFullURL of this image - * @public - */ - function getEscapeFullURL() { - $this->getTitle(); - return $this->title->escapeFullURL(); - } - - /** - * Return the URL of an image, provided its name. - * - * @param string $name Name of the image, without the leading "Image:" - * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath? - * @return string URL of $name image - * @public - */ - static function imageUrl( $name, $fromSharedDirectory = false ) { - global $wgUploadPath,$wgUploadBaseUrl,$wgSharedUploadPath; - if($fromSharedDirectory) { - $base = ''; - $path = $wgSharedUploadPath; - } else { - $base = $wgUploadBaseUrl; - $path = $wgUploadPath; - } - $url = "{$base}{$path}" . wfGetHashPath($name, $fromSharedDirectory) . "{$name}"; - return wfUrlencode( $url ); - } - - /** - * Returns true if the image file exists on disk. - * @return boolean Whether image file exist on disk. - * @public - */ - function exists() { - $this->load(); - return $this->fileExists; - } - - /** - * @todo document - * @param string $thumbName - * @param string $subdir - * @return string - * @private - */ - function thumbUrlFromName( $thumbName, $subdir = 'thumb' ) { - global $wgUploadPath, $wgUploadBaseUrl, $wgSharedUploadPath; - if($this->fromSharedDirectory) { - $base = ''; - $path = $wgSharedUploadPath; - } else { - $base = $wgUploadBaseUrl; - $path = $wgUploadPath; - } - if ( Image::isHashed( $this->fromSharedDirectory ) ) { - $hashdir = wfGetHashPath($this->name, $this->fromSharedDirectory) . - wfUrlencode( $this->name ); - } else { - $hashdir = ''; - } - $url = "{$base}{$path}/{$subdir}{$hashdir}/" . wfUrlencode( $thumbName ); - return $url; - } - - /** - * @deprecated Use $image->transform()->getUrl() or thumbUrlFromName() - */ - function thumbUrl( $width, $subdir = 'thumb' ) { - $name = $this->thumbName( array( 'width' => $width ) ); - if ( strval( $name ) !== '' ) { - return array( false, $this->thumbUrlFromName( $name, $subdir ) ); - } else { - return array( false, false ); - } - } - - /** - * @return mixed - */ - function getTransformScript() { - global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath; - if ( $this->fromSharedDirectory ) { - $script = $wgSharedThumbnailScriptPath; - } else { - $script = $wgThumbnailScriptPath; - } - if ( $script ) { - return "$script?f=" . urlencode( $this->name ); - } else { - return false; - } - } - - /** - * Get a ThumbnailImage which is the same size as the source - * @param mixed $page - * @return MediaTransformOutput - */ - function getUnscaledThumb( $page = false ) { - if ( $page ) { - $params = array( - 'page' => $page, - 'width' => $this->getWidth( $page ) - ); - } else { - $params = array( 'width' => $this->getWidth() ); - } - return $this->transform( $params ); - } - - /** - * Return the file name of a thumbnail with the specified parameters - * - * @param array $params Handler-specific parameters - * @return string file name of a thumbnail with the specified parameters - * @private - */ - function thumbName( $params ) { - $handler = $this->getHandler(); - if ( !$handler ) { - return null; - } - list( $thumbExt, /* $thumbMime */ ) = self::getThumbType( $this->extension, $this->mime ); - $thumbName = $handler->makeParamString( $params ) . '-' . $this->name; - if ( $thumbExt != $this->extension ) { - $thumbName .= ".$thumbExt"; - } - return $thumbName; - } - - /** - * Create a thumbnail of the image having the specified width/height. - * The thumbnail will not be created if the width is larger than the - * image's width. Let the browser do the scaling in this case. - * The thumbnail is stored on disk and is only computed if the thumbnail - * file does not exist OR if it is older than the image. - * Returns the URL. - * - * Keeps aspect ratio of original image. If both width and height are - * specified, the generated image will be no bigger than width x height, - * and will also have correct aspect ratio. - * - * @param integer $width maximum width of the generated thumbnail - * @param integer $height maximum height of the image (optional) - * @public - */ - function createThumb( $width, $height = -1 ) { - $params = array( 'width' => $width ); - if ( $height != -1 ) { - $params['height'] = $height; - } - $thumb = $this->transform( $params ); - if( is_null( $thumb ) || $thumb->isError() ) return ''; - return $thumb->getUrl(); - } - - /** - * As createThumb, but returns a ThumbnailImage object. This can - * provide access to the actual file, the real size of the thumb, - * and can produce a convenient tag for you. - * - * For non-image formats, this may return a filetype-specific icon. - * - * @param integer $width maximum width of the generated thumbnail - * @param integer $height maximum height of the image (optional) - * @param boolean $render True to render the thumbnail if it doesn't exist, - * false to just return the URL - * - * @return ThumbnailImage or null on failure - * @public - * - * @deprecated use transform() - */ - function getThumbnail( $width, $height=-1, $render = true ) { - $params = array( 'width' => $width ); - if ( $height != -1 ) { - $params['height'] = $height; - } - $flags = $render ? self::RENDER_NOW : 0; - return $this->transform( $params, $flags ); - } - - /** - * Transform a media file - * - * @param array[string]mixed $params An associative array of handler-specific parameters. - * Typical keys are width, height and page. - * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering - * @return MediaTransformOutput - */ - function transform( $params, $flags = 0 ) { - global $wgGenerateThumbnailOnParse, $wgUseSquid, $wgIgnoreImageErrors; - - wfProfileIn( __METHOD__ ); - do { - $handler = $this->getHandler(); - if ( !$handler || !$handler->canRender() ) { - // not a bitmap or renderable image, don't try. - $thumb = $this->iconThumb(); - break; - } - - $script = $this->getTransformScript(); - if ( $script && !($flags & self::RENDER_NOW) ) { - // Use a script to transform on client request - $thumb = $handler->getScriptedTransform( $this, $script, $params ); - break; - } - - $normalisedParams = $params; - $handler->normaliseParams( $this, $normalisedParams ); - $thumbName = $this->thumbName( $normalisedParams ); - $thumbPath = wfImageThumbDir( $this->name, $this->fromSharedDirectory ) . "/$thumbName"; - $thumbUrl = $this->thumbUrlFromName( $thumbName ); - - - if ( !$wgGenerateThumbnailOnParse && !($flags & self::RENDER_NOW ) ) { - $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); - break; - } - - wfDebug( "Doing stat for $thumbPath\n" ); - $this->migrateThumbFile( $thumbName ); - if ( file_exists( $thumbPath ) ) { - $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); - break; - } - - $thumb = $handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); - - // Ignore errors if requested - if ( !$thumb ) { - $thumb = null; - } elseif ( $thumb->isError() ) { - $this->lastError = $thumb->toText(); - if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) { - $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); - } - } - - if ( $wgUseSquid ) { - wfPurgeSquidServers( array( $thumbUrl ) ); - } - } while (false); - - wfProfileOut( __METHOD__ ); - return $thumb; - } - - /** - * Fix thumbnail files from 1.4 or before, with extreme prejudice - * @param string $thumbName File name of thumbnail. - * @return void - */ - function migrateThumbFile( $thumbName ) { - $thumbDir = wfImageThumbDir( $this->name, $this->fromSharedDirectory ); - $thumbPath = "$thumbDir/$thumbName"; - if ( is_dir( $thumbPath ) ) { - // Directory where file should be - // This happened occasionally due to broken migration code in 1.5 - // Rename to broken-* - global $wgUploadDirectory; - for ( $i = 0; $i < 100 ; $i++ ) { - $broken = "$wgUploadDirectory/broken-$i-$thumbName"; - if ( !file_exists( $broken ) ) { - rename( $thumbPath, $broken ); - break; - } - } - // Doesn't exist anymore - clearstatcache(); - } - if ( is_file( $thumbDir ) ) { - // File where directory should be - unlink( $thumbDir ); - // Doesn't exist anymore - clearstatcache(); - } - } - - /** - * Get a MediaHandler instance for this image - */ - function getHandler() { - return MediaHandler::getHandler( $this->getMimeType() ); - } - - /** - * Get a ThumbnailImage representing a file type icon - * @return ThumbnailImage - */ - function iconThumb() { - global $wgStylePath, $wgStyleDirectory; - - $icons = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' ); - foreach( $icons as $icon ) { - $path = '/common/images/icons/' . $icon; - $filepath = $wgStyleDirectory . $path; - if( file_exists( $filepath ) ) { - return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); - } - } - return null; - } - - /** - * Get last thumbnailing error. - * Largely obsolete. - * @return mixed - */ - function getLastError() { - return $this->lastError; - } - - /** - * Get all thumbnail names previously generated for this image - * @param boolean $shared - * @return array[]string - */ - function getThumbnails( $shared = false ) { - if ( Image::isHashed( $shared ) ) { - $this->load(); - $files = array(); - $dir = wfImageThumbDir( $this->name, $shared ); - - if ( is_dir( $dir ) ) { - $handle = opendir( $dir ); - - if ( $handle ) { - while ( false !== ( $file = readdir($handle) ) ) { - if ( $file[0] != '.' ) { - $files[] = $file; - } - } - closedir( $handle ); - } - } - } else { - $files = array(); - } - - return $files; - } - - /** - * Refresh metadata in memcached, but don't touch thumbnails or squid - * @return void - */ - function purgeMetadataCache() { - clearstatcache(); - $this->loadFromFile(); - $this->saveToCache(); - } - - /** - * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid - * @param array $archiveFiles - * @param boolean $shared - * @return void - */ - function purgeCache( $archiveFiles = array(), $shared = false ) { - global $wgUseSquid; - - // Refresh metadata cache - $this->purgeMetadataCache(); - - // Delete thumbnails - $files = $this->getThumbnails( $shared ); - $dir = wfImageThumbDir( $this->name, $shared ); - $urls = array(); - foreach ( $files as $file ) { - # Check that the base image name is part of the thumb name - # This is a basic sanity check to avoid erasing unrelated directories - if ( strpos( $file, $this->name ) !== false ) { - $url = $this->thumbUrlFromName( $file ); - $urls[] = $url; - @unlink( "$dir/$file" ); - } - } - - // Purge the squid - if ( $wgUseSquid ) { - $urls[] = $this->getURL(); - foreach ( $archiveFiles as $file ) { - $urls[] = wfImageArchiveUrl( $file ); - } - wfPurgeSquidServers( $urls ); - } - } - - /** - * Purge the image description page, but don't go after - * pages using the image. Use when modifying file history - * but not the current data. - * @return void - */ - function purgeDescription() { - $page = Title::makeTitle( NS_IMAGE, $this->name ); - $page->invalidateCache(); - $page->purgeSquid(); - } - - /** - * Purge metadata and all affected pages when the image is created, - * deleted, or majorly updated. - * @param array $urlArray A set of additional URLs may be passed to purge, - * such as specific image files which have changed (param not used?) - * @return void - */ - function purgeEverything( $urlArr=array() ) { - // Delete thumbnails and refresh image metadata cache - $this->purgeCache(); - $this->purgeDescription(); - - // Purge cache of all pages using this image - $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); - $update->doUpdate(); - } - - /** - * Return the image history of this image, line by line. - * starts with current version, then old versions. - * uses $this->historyLine to check which line to return: - * 0 return line for current version - * 1 query for old versions, return first one - * 2, ... return next old version from above query - * - * @public - * @return mixed false on no next history, object otherwise. - */ - function nextHistoryLine() { - $dbr = wfGetDB( DB_SLAVE ); - - if ( $this->historyLine == 0 ) {// called for the first time, return line from cur - $this->historyRes = $dbr->select( 'image', - array( - 'img_size', - 'img_description', - 'img_user','img_user_text', - 'img_timestamp', - 'img_width', - 'img_height', - "'' AS oi_archive_name" - ), - array( 'img_name' => $this->title->getDBkey() ), - __METHOD__ - ); - if ( 0 == $dbr->numRows( $this->historyRes ) ) { - return FALSE; - } - } else if ( $this->historyLine == 1 ) { - $this->historyRes = $dbr->select( 'oldimage', - array( - 'oi_size AS img_size', - 'oi_description AS img_description', - 'oi_user AS img_user', - 'oi_user_text AS img_user_text', - 'oi_timestamp AS img_timestamp', - 'oi_width as img_width', - 'oi_height as img_height', - 'oi_archive_name' - ), - array( 'oi_name' => $this->title->getDBkey() ), - __METHOD__, - array( 'ORDER BY' => 'oi_timestamp DESC' ) - ); - } - $this->historyLine ++; - - return $dbr->fetchObject( $this->historyRes ); - } - - /** - * Reset the history pointer to the first element of the history - * @public - * @return void - */ - function resetHistory() { - $this->historyLine = 0; - } - - /** - * Return the full filesystem path to the file. Note that this does - * not mean that a file actually exists under that location. - * - * This path depends on whether directory hashing is active or not, - * i.e. whether the images are all found in the same directory, - * or in hashed paths like /images/3/3c. - * - * @public - * @param boolean $fromSharedDirectory Return the path to the file - * in a shared repository (see $wgUseSharedRepository and related - * options in DefaultSettings.php) instead of a local one. - * @return string Full filesystem path to the file. - */ - function getFullPath( $fromSharedRepository = false ) { - global $wgUploadDirectory, $wgSharedUploadDirectory; - - $dir = $fromSharedRepository ? $wgSharedUploadDirectory : - $wgUploadDirectory; - - // $wgSharedUploadDirectory may be false, if thumb.php is used - if ( $dir ) { - $fullpath = $dir . wfGetHashPath($this->name, $fromSharedRepository) . $this->name; - } else { - $fullpath = false; - } - - return $fullpath; - } - - /** - * @param boolean $shared - * @return bool - */ - public static function isHashed( $shared ) { - global $wgHashedUploadDirectory, $wgHashedSharedUploadDirectory; - return $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory; - } - - /** - * Record an image upload in the upload log and the image table - * @param string $oldver - * @param string $desc - * @param string $license - * @param string $copyStatus - * @param string $source - * @param boolean $watch - * @return boolean - */ - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { - global $wgUser, $wgUseCopyrightUpload; - - $dbw = wfGetDB( DB_MASTER ); - - // Delete thumbnails and refresh the metadata cache - $this->purgeCache(); - - // Fail now if the image isn't there - if ( !$this->fileExists || $this->fromSharedDirectory ) { - wfDebug( "Image::recordUpload: File ".$this->imagePath." went missing!\n" ); - return false; - } - - if ( $wgUseCopyrightUpload ) { - if ( $license != '' ) { - $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } - $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" . - '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . - "$licensetxt" . - '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; - } else { - if ( $license != '' ) { - $filedesc = $desc == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n"; - $textdesc = $filedesc . - '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } else { - $textdesc = $desc; - } - } - - $now = $dbw->timestamp(); - - #split mime type - if (strpos($this->mime,'/')!==false) { - list($major,$minor)= explode('/',$this->mime,2); - } - else { - $major= $this->mime; - $minor= "unknown"; - } - - # Test to see if the row exists using INSERT IGNORE - # This avoids race conditions by locking the row until the commit, and also - # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. - $dbw->insert( 'image', - array( - 'img_name' => $this->name, - 'img_size'=> $this->size, - 'img_width' => intval( $this->width ), - 'img_height' => intval( $this->height ), - 'img_bits' => $this->bits, - 'img_media_type' => $this->type, - 'img_major_mime' => $major, - 'img_minor_mime' => $minor, - 'img_timestamp' => $now, - 'img_description' => $desc, - 'img_user' => $wgUser->getID(), - 'img_user_text' => $wgUser->getName(), - 'img_metadata' => $this->metadata, - ), - __METHOD__, - 'IGNORE' - ); - - if( $dbw->affectedRows() == 0 ) { - # Collision, this is an update of an image - # Insert previous contents into oldimage - $dbw->insertSelect( 'oldimage', 'image', - array( - 'oi_name' => 'img_name', - 'oi_archive_name' => $dbw->addQuotes( $oldver ), - 'oi_size' => 'img_size', - 'oi_width' => 'img_width', - 'oi_height' => 'img_height', - 'oi_bits' => 'img_bits', - 'oi_timestamp' => 'img_timestamp', - 'oi_description' => 'img_description', - 'oi_user' => 'img_user', - 'oi_user_text' => 'img_user_text', - ), array( 'img_name' => $this->name ), __METHOD__ - ); - - # Update the current image row - $dbw->update( 'image', - array( /* SET */ - 'img_size' => $this->size, - 'img_width' => intval( $this->width ), - 'img_height' => intval( $this->height ), - 'img_bits' => $this->bits, - 'img_media_type' => $this->type, - 'img_major_mime' => $major, - 'img_minor_mime' => $minor, - 'img_timestamp' => $now, - 'img_description' => $desc, - 'img_user' => $wgUser->getID(), - 'img_user_text' => $wgUser->getName(), - 'img_metadata' => $this->metadata, - ), array( /* WHERE */ - 'img_name' => $this->name - ), __METHOD__ - ); - } else { - # This is a new image - # Update the image count - $site_stats = $dbw->tableName( 'site_stats' ); - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); - } - - $descTitle = $this->getTitle(); - $article = new Article( $descTitle ); - $minor = false; - $watch = $watch || $wgUser->isWatched( $descTitle ); - $suppressRC = true; // There's already a log entry, so don't double the RC load - - if( $descTitle->exists() ) { - // TODO: insert a null revision into the page history for this update. - if( $watch ) { - $wgUser->addWatch( $descTitle ); - } - - # Invalidate the cache for the description page - $descTitle->invalidateCache(); - $descTitle->purgeSquid(); - } else { - // New image; create the description page. - $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC ); - } - - # Hooks, hooks, the magic of hooks... - wfRunHooks( 'FileUpload', array( $this ) ); - - # Add the log entry - $log = new LogPage( 'upload' ); - $log->addEntry( 'upload', $descTitle, $desc ); - - # Commit the transaction now, in case something goes wrong later - # The most important thing is that images don't get lost, especially archives - $dbw->immediateCommit(); - - # Invalidate cache for all pages using this image - $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); - $update->doUpdate(); - - return true; - } - - /** - * Get an array of Title objects which are articles which use this image - * Also adds their IDs to the link cache - * - * This is mostly copied from Title::getLinksTo() - * - * @deprecated Use HTMLCacheUpdate, this function uses too much memory - * @param string $options - * @return array[int]Title - */ - function getLinksTo( $options = '' ) { - wfProfileIn( __METHOD__ ); - - if ( $options ) { - $db = wfGetDB( DB_MASTER ); - } else { - $db = wfGetDB( DB_SLAVE ); - } - $linkCache =& LinkCache::singleton(); - - list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' ); - $encName = $db->addQuotes( $this->name ); - $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; - $res = $db->query( $sql, __METHOD__ ); - - $retVal = array(); - if ( $db->numRows( $res ) ) { - while ( $row = $db->fetchObject( $res ) ) { - if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) { - $linkCache->addGoodLinkObj( $row->page_id, $titleObj ); - $retVal[] = $titleObj; - } - } - } - $db->freeResult( $res ); - wfProfileOut( __METHOD__ ); - return $retVal; - } - - /** - * @return array - */ - function getExifData() { - $handler = $this->getHandler(); - if ( !$handler || $handler->getMetadataType( $this ) != 'exif' ) { - return array(); - } - if ( !$this->metadata ) { - return array(); - } - $exif = unserialize( $this->metadata ); - if ( !$exif ) { - return array(); - } - unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); - $format = new FormatExif( $exif ); - - return $format->getFormattedData(); - } - - /** - * Returns true if the image does not come from the shared - * image repository. - * - * @return bool - */ - function isLocal() { - return !$this->fromSharedDirectory; - } - - /** - * Was this image ever deleted from the wiki? - * - * @return bool - */ - function wasDeleted() { - $title = Title::makeTitle( NS_IMAGE, $this->name ); - return ( $title->isDeleted() > 0 ); - } - - /** - * Delete all versions of the image. - * - * Moves the files into an archive directory (or deletes them) - * and removes the database rows. - * - * Cache purging is done; logging is caller's responsibility. - * - * @param string $reason - * @param boolean $suppress - * @return boolean true on success, false on some kind of failure - */ - function delete( $reason, $suppress=false ) { - $transaction = new FSTransaction(); - $urlArr = array( $this->getURL() ); - - if( !FileStore::lock() ) { - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); - return false; - } - - try { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - - // Delete old versions - $result = $dbw->select( 'oldimage', - array( 'oi_archive_name' ), - array( 'oi_name' => $this->name ) ); - - while( $row = $dbw->fetchObject( $result ) ) { - $oldName = $row->oi_archive_name; - - $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) ); - - // We'll need to purge this URL from caches... - $urlArr[] = wfImageArchiveUrl( $oldName ); - } - $dbw->freeResult( $result ); - - // And the current version... - $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) ); - - $dbw->immediateCommit(); - } catch( MWException $e ) { - wfDebug( __METHOD__.": db error, rolling back file transactions\n" ); - $transaction->rollback(); - FileStore::unlock(); - throw $e; - } - - wfDebug( __METHOD__.": deleted db items, applying file transactions\n" ); - $transaction->commit(); - FileStore::unlock(); - - - // Update site_stats - $site_stats = $dbw->tableName( 'site_stats' ); - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); - - $this->purgeEverything( $urlArr ); - - return true; - } - - - /** - * Delete an old version of the image. - * - * Moves the file into an archive directory (or deletes it) - * and removes the database row. - * - * Cache purging is done; logging is caller's responsibility. - * - * @param string $archiveName - * @param string $reason - * @param boolean $suppress - * @throws MWException or FSException on database or filestore failure - * @return boolean true on success, false on some kind of failure - */ - function deleteOld( $archiveName, $reason, $suppress=false ) { - $transaction = new FSTransaction(); - $urlArr = array(); - - if( !FileStore::lock() ) { - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); - return false; - } - - $transaction = new FSTransaction(); - try { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) ); - $dbw->immediateCommit(); - } catch( MWException $e ) { - wfDebug( __METHOD__.": db error, rolling back file transaction\n" ); - $transaction->rollback(); - FileStore::unlock(); - throw $e; - } - - wfDebug( __METHOD__.": deleted db items, applying file transaction\n" ); - $transaction->commit(); - FileStore::unlock(); - - $this->purgeDescription(); - - // Squid purging - global $wgUseSquid; - if ( $wgUseSquid ) { - $urlArr = array( - wfImageArchiveUrl( $archiveName ), - ); - wfPurgeSquidServers( $urlArr ); - } - return true; - } - - /** - * Delete the current version of a file. - * May throw a database error. - * @param string $reason - * @param boolean $suppress - * @return boolean true on success, false on failure - */ - private function prepareDeleteCurrent( $reason, $suppress=false ) { - return $this->prepareDeleteVersion( - $this->getFullPath(), - $reason, - 'image', - array( - 'fa_name' => 'img_name', - 'fa_archive_name' => 'NULL', - 'fa_size' => 'img_size', - 'fa_width' => 'img_width', - 'fa_height' => 'img_height', - 'fa_metadata' => 'img_metadata', - 'fa_bits' => 'img_bits', - 'fa_media_type' => 'img_media_type', - 'fa_major_mime' => 'img_major_mime', - 'fa_minor_mime' => 'img_minor_mime', - 'fa_description' => 'img_description', - 'fa_user' => 'img_user', - 'fa_user_text' => 'img_user_text', - 'fa_timestamp' => 'img_timestamp' ), - array( 'img_name' => $this->name ), - $suppress, - __METHOD__ ); - } - - /** - * Delete a given older version of a file. - * May throw a database error. - * @param string $archiveName - * @param string $reason - * @param boolean $suppress - * @return boolean true on success, false on failure - */ - private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) { - $oldpath = wfImageArchiveDir( $this->name ) . - DIRECTORY_SEPARATOR . $archiveName; - return $this->prepareDeleteVersion( - $oldpath, - $reason, - 'oldimage', - array( - 'fa_name' => 'oi_name', - 'fa_archive_name' => 'oi_archive_name', - 'fa_size' => 'oi_size', - 'fa_width' => 'oi_width', - 'fa_height' => 'oi_height', - 'fa_metadata' => 'NULL', - 'fa_bits' => 'oi_bits', - 'fa_media_type' => 'NULL', - 'fa_major_mime' => 'NULL', - 'fa_minor_mime' => 'NULL', - 'fa_description' => 'oi_description', - 'fa_user' => 'oi_user', - 'fa_user_text' => 'oi_user_text', - 'fa_timestamp' => 'oi_timestamp' ), - array( - 'oi_name' => $this->name, - 'oi_archive_name' => $archiveName ), - $suppress, - __METHOD__ ); - } - - /** - * Do the dirty work of backing up an image row and its file - * (if $wgSaveDeletedFiles is on) and removing the originals. - * - * Must be run while the file store is locked and a database - * transaction is open to avoid race conditions. - * - * @return FSTransaction - */ - private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) { - global $wgUser, $wgSaveDeletedFiles; - - // Dupe the file into the file store - if( file_exists( $path ) ) { - if( $wgSaveDeletedFiles ) { - $group = 'deleted'; - - $store = FileStore::get( $group ); - $key = FileStore::calculateKey( $path, $this->extension ); - $transaction = $store->insert( $key, $path, - FileStore::DELETE_ORIGINAL ); - } else { - $group = null; - $key = null; - $transaction = FileStore::deleteFile( $path ); - } - } else { - wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" ); - $group = null; - $key = null; - $transaction = new FSTransaction(); // empty - } - - if( $transaction === false ) { - // Fail to restore? - wfDebug( __METHOD__.": import to file store failed, aborting\n" ); - throw new MWException( "Could not archive and delete file $path" ); - return false; - } - - // Bitfields to further supress the image content - // Note that currently, live images are stored elsewhere - // and cannot be partially deleted - $bitfield = 0; - if ( $suppress ) { - $bitfield |= self::DELETED_FILE; - $bitfield |= self::DELETED_COMMENT; - $bitfield |= self::DELETED_USER; - $bitfield |= self::DELETED_RESTRICTED; - } - - $dbw = wfGetDB( DB_MASTER ); - $storageMap = array( - 'fa_storage_group' => $dbw->addQuotes( $group ), - 'fa_storage_key' => $dbw->addQuotes( $key ), - - 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ), - 'fa_deleted_timestamp' => $dbw->timestamp(), - 'fa_deleted_reason' => $dbw->addQuotes( $reason ), - 'fa_deleted' => $bitfield); - $allFields = array_merge( $storageMap, $fieldMap ); - - try { - if( $wgSaveDeletedFiles ) { - $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname ); - } - $dbw->delete( $table, $where, $fname ); - } catch( DBQueryError $e ) { - // Something went horribly wrong! - // Leave the file as it was... - wfDebug( __METHOD__.": database error, rolling back file transaction\n" ); - $transaction->rollback(); - throw $e; - } - - return $transaction; - } - - /** - * Restore all or specified deleted revisions to the given file. - * Permissions and logging are left to the caller. - * - * May throw database exceptions on error. - * - * @param $versions set of record ids of deleted items to restore, - * or empty to restore all revisions. - * @return the number of file revisions restored if successful, - * or false on failure - */ - function restore( $versions=array(), $Unsuppress=false ) { - global $wgUser; - - if( !FileStore::lock() ) { - wfDebug( __METHOD__." could not acquire filestore lock\n" ); - return false; - } - - $transaction = new FSTransaction(); - try { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - - // Re-confirm whether this image presently exists; - // if no we'll need to create an image record for the - // first item we restore. - $exists = $dbw->selectField( 'image', '1', - array( 'img_name' => $this->name ), - __METHOD__ ); - - // Fetch all or selected archived revisions for the file, - // sorted from the most recent to the oldest. - $conditions = array( 'fa_name' => $this->name ); - if( $versions ) { - $conditions['fa_id'] = $versions; - } - - $result = $dbw->select( 'filearchive', '*', - $conditions, - __METHOD__, - array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - - if( $dbw->numRows( $result ) < count( $versions ) ) { - // There's some kind of conflict or confusion; - // we can't restore everything we were asked to. - wfDebug( __METHOD__.": couldn't find requested items\n" ); - $dbw->rollback(); - FileStore::unlock(); - return false; - } - - if( $dbw->numRows( $result ) == 0 ) { - // Nothing to do. - wfDebug( __METHOD__.": nothing to do\n" ); - $dbw->rollback(); - FileStore::unlock(); - return true; - } - - $revisions = 0; - while( $row = $dbw->fetchObject( $result ) ) { - if ( $Unsuppress ) { - // Currently, fa_deleted flags fall off upon restore, lets be careful about this - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { - // Skip restoring file revisions that the user cannot restore - continue; - } - $revisions++; - $store = FileStore::get( $row->fa_storage_group ); - if( !$store ) { - wfDebug( __METHOD__.": skipping row with no file.\n" ); - continue; - } - - if( $revisions == 1 && !$exists ) { - $destDir = wfImageDir( $row->fa_name ); - if ( !is_dir( $destDir ) ) { - wfMkdirParents( $destDir ); - } - $destPath = $destDir . DIRECTORY_SEPARATOR . $row->fa_name; - - // We may have to fill in data if this was originally - // an archived file revision. - if( is_null( $row->fa_metadata ) ) { - $tempFile = $store->filePath( $row->fa_storage_key ); - - $magic = MimeMagic::singleton(); - $mime = $magic->guessMimeType( $tempFile, true ); - $media_type = $magic->getMediaType( $tempFile, $mime ); - list( $major_mime, $minor_mime ) = self::splitMime( $mime ); - $handler = MediaHandler::getHandler( $mime ); - if ( $handler ) { - $metadata = $handler->getMetadata( $image, $tempFile ); - } else { - $metadata = ''; - } - } else { - $metadata = $row->fa_metadata; - $major_mime = $row->fa_major_mime; - $minor_mime = $row->fa_minor_mime; - $media_type = $row->fa_media_type; - } - - $table = 'image'; - $fields = array( - 'img_name' => $row->fa_name, - 'img_size' => $row->fa_size, - 'img_width' => $row->fa_width, - 'img_height' => $row->fa_height, - 'img_metadata' => $metadata, - 'img_bits' => $row->fa_bits, - 'img_media_type' => $media_type, - 'img_major_mime' => $major_mime, - 'img_minor_mime' => $minor_mime, - 'img_description' => $row->fa_description, - 'img_user' => $row->fa_user, - 'img_user_text' => $row->fa_user_text, - 'img_timestamp' => $row->fa_timestamp ); - } else { - $archiveName = $row->fa_archive_name; - if( $archiveName == '' ) { - // This was originally a current version; we - // have to devise a new archive name for it. - // Format is ! - $archiveName = - wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) . - '!' . $row->fa_name; - } - $destDir = wfImageArchiveDir( $row->fa_name ); - if ( !is_dir( $destDir ) ) { - wfMkdirParents( $destDir ); - } - $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName; - - $table = 'oldimage'; - $fields = array( - 'oi_name' => $row->fa_name, - 'oi_archive_name' => $archiveName, - 'oi_size' => $row->fa_size, - 'oi_width' => $row->fa_width, - 'oi_height' => $row->fa_height, - 'oi_bits' => $row->fa_bits, - 'oi_description' => $row->fa_description, - 'oi_user' => $row->fa_user, - 'oi_user_text' => $row->fa_user_text, - 'oi_timestamp' => $row->fa_timestamp ); - } - - $dbw->insert( $table, $fields, __METHOD__ ); - // @todo this delete is not totally safe, potentially - $dbw->delete( 'filearchive', - array( 'fa_id' => $row->fa_id ), - __METHOD__ ); - - // Check if any other stored revisions use this file; - // if so, we shouldn't remove the file from the deletion - // archives so they will still work. - $useCount = $dbw->selectField( 'filearchive', - 'COUNT(*)', - array( - 'fa_storage_group' => $row->fa_storage_group, - 'fa_storage_key' => $row->fa_storage_key ), - __METHOD__ ); - if( $useCount == 0 ) { - wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" ); - $flags = FileStore::DELETE_ORIGINAL; - } else { - $flags = 0; - } - - $transaction->add( $store->export( $row->fa_storage_key, - $destPath, $flags ) ); - } - - $dbw->immediateCommit(); - } catch( MWException $e ) { - wfDebug( __METHOD__." caught error, aborting\n" ); - $transaction->rollback(); - throw $e; - } - - $transaction->commit(); - FileStore::unlock(); - - if( $revisions > 0 ) { - if( !$exists ) { - wfDebug( __METHOD__." restored $revisions items, creating a new current\n" ); - - // Update site_stats - $site_stats = $dbw->tableName( 'site_stats' ); - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); - - $this->purgeEverything(); - } else { - wfDebug( __METHOD__." restored $revisions as archived versions\n" ); - $this->purgeDescription(); - } - } - - return $revisions; - } - - /** - * Returns 'true' if this image is a multipage document, e.g. a DJVU - * document. - * - * @return Bool - */ - function isMultipage() { - $handler = $this->getHandler(); - return $handler && $handler->isMultiPage(); - } - - /** - * Returns the number of pages of a multipage document, or NULL for - * documents which aren't multipage documents - */ - function pageCount() { - $handler = $this->getHandler(); - if ( $handler && $handler->isMultiPage() ) { - return $handler->pageCount( $this ); - } else { - return null; - } - } - - static function getCommonsDB() { - static $dbc; - global $wgLoadBalancer, $wgSharedUploadDBname; - if ( !isset( $dbc ) ) { - $i = $wgLoadBalancer->getGroupIndex( 'commons' ); - $dbinfo = $wgLoadBalancer->mServers[$i]; - $dbc = new Database( $dbinfo['host'], $dbinfo['user'], - $dbinfo['password'], $wgSharedUploadDBname ); - } - return $dbc; - } - - /** - * Calculate the height of a thumbnail using the source and destination width - */ - static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { - // Exact integer multiply followed by division - if ( $srcWidth == 0 ) { - return 0; - } else { - return round( $srcHeight * $dstWidth / $srcWidth ); - } - } - - /** - * Get an image size array like that returned by getimagesize(), or false if it - * can't be determined. - * - * @param string $fileName The filename - * @return array - */ - function getImageSize( $fileName ) { - $handler = $this->getHandler(); - return $handler->getImageSize( $this, $fileName ); - } - - /** - * Get the thumbnail extension and MIME type for a given source MIME type - * @return array thumbnail extension and MIME type - */ - static function getThumbType( $ext, $mime ) { - $handler = MediaHandler::getHandler( $mime ); - if ( $handler ) { - return $handler->getThumbType( $ext, $mime ); - } else { - return array( $ext, $mime ); - } - } - -} //class - - -/** - * @addtogroup Media - */ -class ArchivedFile -{ - /** - * Returns a file object from the filearchive table - * In the future, all current and old image storage - * may use FileStore. There will be a "old" storage - * for current and previous file revisions as well as - * the "deleted" group for archived revisions - * @param $title, the corresponding image page title - * @param $id, the image id, a unique key - * @param $key, optional storage key - * @return ResultWrapper - */ - function ArchivedFile( $title, $id=0, $key='' ) { - if( !is_object( $title ) ) { - throw new MWException( 'Image constructor given bogus title.' ); - } - $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'"; - if( $title->getNamespace() == NS_IMAGE ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'filearchive', - array( - 'fa_id', - 'fa_name', - 'fa_storage_key', - 'fa_storage_group', - 'fa_size', - 'fa_bits', - 'fa_width', - 'fa_height', - 'fa_metadata', - 'fa_media_type', - 'fa_major_mime', - 'fa_minor_mime', - 'fa_description', - 'fa_user', - 'fa_user_text', - 'fa_timestamp', - 'fa_deleted' ), - array( - 'fa_name' => $title->getDbKey(), - $conds ), - __METHOD__, - array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - - if ( $dbr->numRows( $res ) == 0 ) { - // this revision does not exist? - return; - } - $ret = $dbr->resultObject( $res ); - $row = $ret->fetchObject(); - - // initialize fields for filestore image object - $this->mId = intval($row->fa_id); - $this->mName = $row->fa_name; - $this->mGroup = $row->fa_storage_group; - $this->mKey = $row->fa_storage_key; - $this->mSize = $row->fa_size; - $this->mBits = $row->fa_bits; - $this->mWidth = $row->fa_width; - $this->mHeight = $row->fa_height; - $this->mMetaData = $row->fa_metadata; - $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime"; - $this->mType = $row->fa_media_type; - $this->mDescription = $row->fa_description; - $this->mUser = $row->fa_user; - $this->mUserText = $row->fa_user_text; - $this->mTimestamp = $row->fa_timestamp; - $this->mDeleted = $row->fa_deleted; - } else { - throw new MWException( 'This title does not correspond to an image page.' ); - return; - } - return true; - } - - /** - * int $field one of DELETED_* bitfield constants - * for file or revision rows - * @return bool - */ - function isDeleted( $field ) { - return ($this->mDeleted & $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 - * @return bool - */ - function userCan( $field ) { - if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) { - // images - global $wgUser; - $permission = ( $this->mDeleted & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED - ? 'hiderevision' - : 'deleterevision'; - wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); - return $wgUser->isAllowed( $permission ); - } else { - return true; - } - } -} - -/** - * Aliases for backwards compatibility with 1.6 - */ -define( 'MW_IMG_DELETED_FILE', Image::DELETED_FILE ); -define( 'MW_IMG_DELETED_COMMENT', Image::DELETED_COMMENT ); -define( 'MW_IMG_DELETED_USER', Image::DELETED_USER ); -define( 'MW_IMG_DELETED_RESTRICTED', Image::DELETED_RESTRICTED ); - -?> diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index d04110d447..f7c73dbf88 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -1,113 +1,4 @@ mImages[] = array( &$image, $html ); - wfDebug( "ImageGallery::add " . $image->getName() . "\n" ); + function add( $title, $html='' ) { + if ( $title instanceof File ) { + // Old calling convention + $title = $title->getTitle(); + } + $this->mImages[] = array( $title, $html ); + wfDebug( "ImageGallery::add " . $title->getText() . "\n" ); } /** * Add an image at the beginning of the gallery. * - * @param $image Image object that is added to the gallery + * @param $title Title object of the image that is added to the gallery * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. */ - function insert( $image, $html='' ) { - array_unshift( $this->mImages, array( &$image, $html ) ); + function insert( $title, $html='' ) { + array_unshift( $this->mImages, array( &$title, $html ) ); } @@ -195,12 +199,12 @@ class ImageGallery $params = array( 'width' => $this->mWidths, 'height' => $this->mHeights ); $i = 0; foreach ( $this->mImages as $pair ) { - $img =& $pair[0]; + $nt = $pair[0]; $text = $pair[1]; - $nt = $img->getTitle(); + $img = wfFindFile( $nt ); - if( $nt->getNamespace() != NS_IMAGE ) { + if( $nt->getNamespace() != NS_IMAGE || !$img ) { # We're dealing with a non-image, spit out the name and be done with it. $thumbhtml = "\n\t\t\t".'
' . htmlspecialchars( $nt->getText() ) . '
'; @@ -222,7 +226,7 @@ class ImageGallery //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut ); if( $this->mShowBytes ) { - if( $img->exists() ) { + if( $img ) { $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), $wgLang->formatNum( $img->getSize() ) ); } else { diff --git a/includes/ImagePage.php b/includes/ImagePage.php index ac976094e3..f3109c5f03 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -18,6 +18,14 @@ class ImagePage extends Article { /* private */ var $img; // Image object this page is shown for var $mExtraDescription = false; + function __construct( $title ) { + parent::__construct( $title ); + $this->img = wfFindFile( $this->mTitle ); + if ( !$this->img ) { + $this->img = wfLocalFile( $this->mTitle ); + } + } + /** * Handler for action=render * Include body text only; none of the image extras @@ -31,8 +39,6 @@ class ImagePage extends Article { function view() { global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; - $this->img = new Image( $this->mTitle ); - $diff = $wgRequest->getVal( 'diff' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); @@ -160,7 +166,7 @@ class ImagePage extends Article { * shared upload server if possible. */ function getContent() { - if( $this->img && $this->img->fromSharedDirectory && 0 == $this->getID() ) { + if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) { return ''; } return Article::getContent(); @@ -332,26 +338,26 @@ class ImagePage extends Article { $dirmark = $wgContLang->getDirMark(); if (!$this->img->isSafeFile()) { $warning = wfMsg( 'mediawarning' ); - $wgOut->addWikiText( <<addWikiText( <<$infores [[Media:$filename|$filename]]$dirmark $info
$warning
-END +EOT ); } else { - $wgOut->addWikiText( <<addWikiText( <<$infores [[Media:$filename|$filename]]$dirmark $info -END +EOT ); } } - if($this->img->fromSharedDirectory) { + if(!$this->img->isLocal()) { $this->printSharedImageText(); } } else { @@ -365,27 +371,21 @@ END } function printSharedImageText() { - global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut, $wgUser; - - $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey()); - $sharedtext = "
" . wfMsgWikiHtml("sharedupload"); - if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) { + global $wgOut, $wgUser; + $descUrl = $this->img->getDescriptionUrl(); + $descText = $this->img->getDescriptionText(); + $s = "
" . wfMsgWikiHtml("sharedupload"); + if ( $descUrl && !$descText) { $sk = $wgUser->getSkin(); - $title = SpecialPage::getTitleFor( 'Upload' ); - $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'), - array( 'wpDestFile' => urlencode( $this->img->getName() ))); - $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link); + $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') ); + $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link); } - $sharedtext .= "
"; - $wgOut->addHTML($sharedtext); + $s .= "
"; + $wgOut->addHTML($s); - if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { - $renderUrl = wfAppendQuery( $url, 'action=render' ); - wfDebug( "Fetching shared description from $renderUrl\n" ); - $text = Http::get( $renderUrl ); - if ($text) - $this->mExtraDescription = $text; + if ( $descText ) { + $this->mExtraDescription = $descText; } } @@ -402,7 +402,7 @@ END function uploadLinksBox() { global $wgUser, $wgOut; - if( $this->img->fromSharedDirectory ) + if( !$this->img->isLocal() ) return; $sk = $wgUser->getSkin(); @@ -441,7 +441,7 @@ END $line = $this->img->nextHistoryLine(); if ( $line ) { - $list = new ImageHistoryList( $sk ); + $list = new ImageHistoryList( $sk, $this->img ); $s = $list->beginImageHistoryList() . $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp), $this->mTitle->getDBkey(), $line->img_user, @@ -530,8 +530,6 @@ END return; } - $this->img = new Image( $this->mTitle ); - # Deleting old images doesn't require confirmation if ( !is_null( $oldimage ) || $confirm ) { if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { @@ -655,39 +653,23 @@ END $wgOut->showErrorPage( 'internalerror', 'sessionfailure' ); return; } - $name = substr( $oldimage, 15 ); - - $dest = wfImageDir( $name ); - $archive = wfImageArchiveDir( $name ); - $curfile = "{$dest}/{$name}"; - if ( !is_dir( $dest ) ) wfMkdirParents( $dest ); - if ( !is_dir( $archive ) ) wfMkdirParents( $archive ); + $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage ); + $result = $this->img->publish( $sourcePath ); - if ( ! is_file( $curfile ) ) { - $wgOut->showFileNotFoundError( htmlspecialchars( $curfile ) ); - return; - } - $oldver = wfTimestampNow() . "!{$name}"; - - if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) { - $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" ); - return; - } - if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) { - $wgOut->showFileCopyError( "${archive}/{$oldimage}", $curfile ); + if ( WikiError::isError( $result ) ) { + $this->showError( $result ); return; } # Record upload and update metadata cache - $img = Image::newFromName( $name ); - $img->recordUpload( $oldver, wfMsg( "reverted" ) ); + $this->img->recordUpload( $result, wfMsg( "reverted" ) ); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->addHTML( wfMsg( 'imagereverted' ) ); - $descTitle = $img->getTitle(); + $descTitle = $this->img->getTitle(); $wgOut->returnToMain( false, $descTitle->getPrefixedText() ); } @@ -695,7 +677,6 @@ END * Override handling of action=purge */ function doPurge() { - $this->img = new Image( $this->mTitle ); if( $this->img->exists() ) { wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); @@ -708,6 +689,18 @@ END parent::doPurge(); } + /** + * Display an error from a wikitext-formatted WikiError object + */ + function showError( WikiError $error ) { + global $wgOut; + $wgOut->setPageTitle( wfMsg( "internalerror" ) ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->addWikiText( $error->getMessage() ); + } + } /** @@ -715,8 +708,10 @@ END * @addtogroup Media */ class ImageHistoryList { - function ImageHistoryList( &$skin ) { - $this->skin =& $skin; + var $img, $skin; + function ImageHistoryList( $skin, $img ) { + $this->skin = $skin; + $this->img = $img; } function beginImageHistoryList() { @@ -738,11 +733,12 @@ class ImageHistoryList { $del = wfMsgHtml( 'deleteimg' ); $delall = wfMsgHtml( 'deleteimgcompletely' ); $cur = wfMsgHtml( 'cur' ); + $local = $this->img->isLocal(); if ( $iscur ) { - $url = Image::imageUrl( $img ); + $url = htmlspecialchars( $this->img->getURL() ); $rlink = $cur; - if ( $wgUser->isAllowed('delete') ) { + if ( $local && $wgUser->isAllowed('delete') ) { $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() . '&action=delete' ); $style = $this->skin->getInternalLinkAttributes( $link, $delall ); @@ -752,8 +748,8 @@ class ImageHistoryList { $dlink = $del; } } else { - $url = htmlspecialchars( wfImageArchiveUrl( $img ) ); - if( $wgUser->getID() != 0 && $wgTitle->userCan( 'edit' ) ) { + $url = htmlspecialchars( $this->img->getArchiveUrl( $img ) ); + if( $local && $wgUser->getID() != 0 && $wgTitle->userCan( 'edit' ) ) { $token = urlencode( $wgUser->editToken( $img ) ); $rlink = $this->skin->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'revertimg' ), 'action=revert&oldimage=' . @@ -769,8 +765,12 @@ class ImageHistoryList { $dlink = $del; } } - - $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); + + if ( $local ) { + $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); + } else { + $userlink = htmlspecialchars( $usertext ); + } $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( $size ) ); $widthheight = wfMsgHtml( 'widthheight', $width, $height ); @@ -782,7 +782,6 @@ class ImageHistoryList { $s .= "\n"; return $s; } - } diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php index 93f090a107..a2caa313ef 100644 --- a/includes/ImageQueryPage.php +++ b/includes/ImageQueryPage.php @@ -30,8 +30,8 @@ class ImageQueryPage extends QueryPage { # $num [should update this to use a Pager] for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) { $image = $this->prepareImage( $row ); - if( $image instanceof Image ) { - $gallery->add( $image, $this->getCellHtml( $row ) ); + if( $image ) { + $gallery->add( $image->getTitle(), $this->getCellHtml( $row ) ); } } @@ -49,7 +49,7 @@ class ImageQueryPage extends QueryPage { $namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE; $title = Title::makeTitleSafe( $namespace, $row->title ); return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE ) - ? new Image( $title ) + ? wfFindFile( $title ) : null; } diff --git a/includes/Linker.php b/includes/Linker.php index 326b3bc12c..6abcaa3e8c 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -440,13 +440,14 @@ class Linker { * @return string */ function makeImageLinkObj( $nt, $label, $alt, $align = '', $params = array(), $framed = false, - $thumb = false, $manual_thumb = '', $valign = '' ) + $thumb = false, $manual_thumb = '', $valign = '', $time = false ) { global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; - $img = new Image( $nt ); + $img = wfFindFile( $nt, $time ); - if ( !$img->allowInlineDisplay() && $img->exists() ) { + if ( $img && !$img->allowInlineDisplay() ) { + wfDebug( __METHOD__.': '.$nt->getPrefixedDBkey()." does not allow inline display\n" ); return $this->makeKnownLinkObj( $nt ); } @@ -459,7 +460,7 @@ class Linker { $postfix = ''; $align = 'none'; } - if ( !isset( $params['width'] ) ) { + if ( $img && !isset( $params['width'] ) ) { $params['width'] = $img->getWidth( $page ); if( $thumb || $framed || isset( $params['frameless'] ) ) { $wopt = $wgUser->getOption( 'thumbsize' ); @@ -490,10 +491,10 @@ class Linker { if ( $align == '' ) { $align = $wgContLang->isRTL() ? 'left' : 'right'; } - return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix; + return $prefix.$this->makeThumbLinkObj( $nt, $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix; } - if ( $params['width'] && $img->exists() ) { + if ( $img && $params['width'] ) { # Create a resized image, without the additional thumbnail features $thumb = $img->transform( $params ); } else { @@ -524,7 +525,7 @@ class Linker { ); if ( !$thumb ) { - $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); + $s = $this->makeBrokenImageLinkObj( $nt ); } else { $s = $thumb->toHtml( $imgAttribs, $linkAttribs ); } @@ -536,10 +537,12 @@ class Linker { /** * Make HTML for a thumbnail including image, border and caption - * $img is an Image object + * @param Title $nt + * @param Image $img Image object or false if it doesn't exist */ - function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) { + function makeThumbLinkObj( Title $nt, $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) { global $wgStylePath, $wgContLang; + $exists = $img && $img->exists(); $page = isset( $params['page'] ) ? $params['page'] : false; @@ -548,45 +551,54 @@ class Linker { $params['width'] = isset( $params['upright'] ) ? 130 : 180; } $thumb = false; - if ( $manual_thumb != '' ) { - # Use manually specified thumbnail - $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); - if( $manual_title ) { - $manual_img = new Image( $manual_title ); - $thumb = $manual_img->getUnscaledThumb(); - } - } elseif ( $framed ) { - // Use image dimensions, don't scale - $thumb = $img->getUnscaledThumb( $page ); + + if ( !$exists ) { + $outerWidth = $params['width'] + 2; } else { - # Do not present an image bigger than the source, for bitmap-style images - # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour - $srcWidth = $img->getWidth( $page ); - if ( $srcWidth && !$img->mustRender() && $params['width'] > $srcWidth ) { - $params['width'] = $srcWidth; + if ( $manual_thumb != '' ) { + # Use manually specified thumbnail + $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); + if( $manual_title ) { + $manual_img = wfFindFile( $manual_title ); + if ( $manual_img ) { + $thumb = $manual_img->getUnscaledThumb(); + } else { + $exists = false; + } + } + } elseif ( $framed ) { + // Use image dimensions, don't scale + $thumb = $img->getUnscaledThumb( $page ); + } else { + # Do not present an image bigger than the source, for bitmap-style images + # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour + $srcWidth = $img->getWidth( $page ); + if ( $srcWidth && !$img->mustRender() && $params['width'] > $srcWidth ) { + $params['width'] = $srcWidth; + } + $thumb = $img->transform( $params ); } - $thumb = $img->transform( $params ); - } - if ( $thumb ) { - $outerWidth = $thumb->getWidth() + 2; - } else { - $outerWidth = $params['width'] + 2; + if ( $thumb ) { + $outerWidth = $thumb->getWidth() + 2; + } else { + $outerWidth = $params['width'] + 2; + } } $query = $page ? 'page=' . urlencode( $page ) : ''; - $u = $img->getTitle()->getLocalURL( $query ); + $u = $nt->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; $s = "
"; - if ( !$thumb ) { - $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); + if( !$exists ) { + $s .= $this->makeBrokenImageLinkObj( $nt ); $zoomicon = ''; - } elseif( !$img->exists() ) { - $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); + } elseif ( !$thumb ) { + $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); $zoomicon = ''; } else { $imgAttribs = array( @@ -645,10 +657,10 @@ class Linker { return $s; } - /** @todo document */ - function makeMediaLink( $name, /* wtf?! */ $url, $alt = '' ) { + /** @deprecated use Linker::makeMediaLinkObj() */ + function makeMediaLink( $name, $unused = '', $text = '' ) { $nt = Title::makeTitleSafe( NS_IMAGE, $name ); - return $this->makeMediaLinkObj( $nt, $alt ); + return $this->makeMediaLinkObj( $nt, $text ); } /** @@ -666,13 +678,13 @@ class Linker { ### HOTFIX. Instead of breaking, return empty string. return $text; } else { - $img = new Image( $title ); - if( $img->exists() ) { + $img = wfFindFile( $title ); + if( $img ) { $url = $img->getURL(); $class = 'internal'; } else { $upload = SpecialPage::getTitleFor( 'Upload' ); - $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) ); + $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getText() ) ); $class = 'new'; } $alt = htmlspecialchars( $title->getText() ); diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php index f3e024a690..28cdd06793 100644 --- a/includes/MediaTransformOutput.php +++ b/includes/MediaTransformOutput.php @@ -1,7 +1,7 @@ exists() ) { + $img = wfFindFile( $nt ); + if( $img ) { // Force a blue link if the file exists; may be a remote // upload on the shared repository, and we want to see its // auto-generated page. @@ -4393,7 +4393,7 @@ class Parser ); $html = $pout->getText(); - $ig->add( new Image( $nt ), $html ); + $ig->add( $nt, $html ); # Only add real images (bug #5586) if ( $nt->getNamespace() == NS_IMAGE ) { diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 7c627ff3a4..005d8584af 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -122,8 +122,8 @@ class SearchEngine { # There may have been a funny upload, or it may be on a shared # file repository such as Wikimedia Commons. if( $title->getNamespace() == NS_IMAGE ) { - $image = new Image( $title ); - if( $image->exists() ) { + $image = wfFindFile( $title ); + if( $image ) { return $title; } } diff --git a/includes/Setup.php b/includes/Setup.php index 7f964a1d21..3f9746bc17 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -54,6 +54,59 @@ if( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp"; if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; +/** + * Initialise $wgLocalFileRepo from backwards-compatible settings + */ +if ( !$wgLocalFileRepo ) { + $wgLocalFileRepo = array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'directory' => $wgUploadDirectory, + 'url' => $wgUploadBaseUrl ? $wgUploadBaseUrl . $wgUploadPath : $wgUploadPath, + 'hashLevels' => $wgHashedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + ); +} +/** + * Initialise shared repo from backwards-compatible settings + */ +if ( $wgUseSharedUploads ) { + if ( $wgSharedUploadDBname ) { + $wgForeignFileRepos[] = array( + 'class' => 'ForeignDBRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'dbType' => $wgDBtype, + 'dbServer' => $wgDBserver, + 'dbUser' => $wgDBuser, + 'dbPassword' => $wgDBpassword, + 'dbName' => $wgSharedUploadDBname, + 'dbFlags' => DBO_DEFAULT, + 'tablePrefix' => $wgSharedUploadDBprefix, + 'hasSharedCache' => $wgCacheSharedUploads, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } else { + $wgForeignFileRepos[] = array( + 'class' => 'FSRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } +} + require_once( "$IP/includes/AutoLoader.php" ); wfProfileIn( $fname.'-exception' ); diff --git a/includes/Skin.php b/includes/Skin.php index e53d579200..f10e0950b5 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -740,8 +740,8 @@ END; if ( $wgOut->isArticleRelated() ) { if ( $wgTitle->getNamespace() == NS_IMAGE ) { $name = $wgTitle->getDBkey(); - $image = new Image( $wgTitle ); - if( $image->exists() ) { + $image = wfFindFile( $wgTitle ); + if( $image ) { $link = htmlspecialchars( $image->getURL() ); $style = $this->getInternalLinkAttributes( $link, $name ); $s .= " | {$name}"; diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php index 59fc78f50e..5eed551293 100644 --- a/includes/SpecialImagelist.php +++ b/includes/SpecialImagelist.php @@ -115,7 +115,9 @@ class ImageListPager extends TablePager { case 'img_name': $name = $this->mCurrentRow->img_name; $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value ); - $download = Xml::element('a', array( "href" => Image::imageUrl( $name ) ), $this->mMessages['imgfile'] ); + $image = wfLocalFile( $value ); + $url = $image->getURL(); + $download = Xml::element('a', array( "href" => $url ), $this->mMessages['imgfile'] ); return "$link ($download)"; case 'img_user_text': if ( $this->mCurrentRow->img_user ) { diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php index 5ecfb35fcb..3e7d12f1c0 100644 --- a/includes/SpecialMIMEsearch.php +++ b/includes/SpecialMIMEsearch.php @@ -66,7 +66,7 @@ class MIMEsearchPage extends QueryPage { $text = $wgContLang->convert( $nt->getText() ); $plink = $skin->makeLink( $nt->getPrefixedText(), $text ); - $download = $skin->makeMediaLink( $nt->getText(), 'fuck me!', wfMsgHtml( 'download' ) ); + $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) ); $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->img_size ) ); $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ), diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php index 72b169b145..43009a740f 100644 --- a/includes/SpecialNewimages.php +++ b/includes/SpecialNewimages.php @@ -135,10 +135,9 @@ function wfSpecialNewimages( $par, $specialPage ) { $ut = $s->img_user_text; $nt = Title::newFromText( $name, NS_IMAGE ); - $img = new Image( $nt ); $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); - $gallery->add( $img, "$ul
\n".$wgLang->timeanddate( $s->img_timestamp, true )."
\n" ); + $gallery->add( $nt, "$ul
\n".$wgLang->timeanddate( $s->img_timestamp, true )."
\n" ); $timestamp = wfTimestamp( TS_MW, $s->img_timestamp ); if( empty( $firstTimestamp ) ) { diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 7a322dbd40..17068334f1 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -269,7 +269,7 @@ class PageArchive { $restoreFiles = $restoreAll || !empty( $fileVersions ); if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) { - $img = new Image( $this->title ); + $img = wfLocalFile( $this->title ); $filesRestored = $img->restore( $fileVersions ); } else { $filesRestored = 0; diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index e3707f13de..79ebef174d 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -27,6 +27,7 @@ class UploadForm { var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload; var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType; var $mUploadTempFileSize = 0; + var $mImage; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value @@ -412,13 +413,13 @@ class UploadForm { global $wgUser; $sk = $wgUser->getSkin(); - $image = new Image( $nt ); + $image = wfLocalFile( $nt ); // Check for uppercase extension. We allow these filenames but check if an image // with lowercase extension exists already if ( $finalExt != strtolower( $finalExt ) ) { $nt_lc = Title::newFromText( $partname . '.' . strtolower( $finalExt ) ); - $image_lc = new Image( $nt_lc ); + $image_lc = wfLocalFile( $nt_lc ); } if( $image->exists() ) { @@ -452,7 +453,7 @@ class UploadForm { } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) { # Check for filenames like 50px- or 180px-, these are mostly thumbnails $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $finalExt ); - $image_thb = new Image( $nt_thb ); + $image_thb = wfLocalFile( $nt_thb ); if ($image_thb->exists() ) { # Check if an image without leading '180px-' (or similiar) exists $dlink = $sk->makeKnownLinkObj( $nt_thb); @@ -500,8 +501,8 @@ class UploadForm { * Update the upload log and create the description page * if it's a new file. */ - $img = Image::newFromName( $this->mUploadSaveName ); - $success = $img->recordUpload( $this->mUploadOldVersion, + $this->mImage = wfLocalFile( $this->mUploadSaveName ); + $success = $this->mImage->recordUpload( $this->mUploadOldVersion, $this->mUploadDescription, $this->mLicense, $this->mUploadCopyStatus, @@ -512,7 +513,7 @@ class UploadForm { $this->showSuccess(); wfRunHooks( 'UploadComplete', array( &$img ) ); } else { - // Image::recordUpload() fails if the image went missing, which is + // File::recordUpload() fails if the image went missing, which is // unlikely, hence the lack of a specialised message $wgOut->showFileNotFoundError( $this->mUploadSaveName ); } @@ -534,48 +535,13 @@ class UploadForm { function saveUploadedFile( $saveName, $tempName, $useRename = false ) { global $wgOut, $wgAllowCopyUploads; - if ( !$useRename AND $wgAllowCopyUploads AND $this->mSourceType == 'web' ) $useRename = true; - - $fname= "SpecialUpload::saveUploadedFile"; - - $dest = wfImageDir( $saveName ); - $archive = wfImageArchiveDir( $saveName ); - if ( !is_dir( $dest ) ) wfMkdirParents( $dest ); - if ( !is_dir( $archive ) ) wfMkdirParents( $archive ); - - $this->mSavedFile = "{$dest}/{$saveName}"; - - if( is_file( $this->mSavedFile ) ) { - $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}"; - wfSuppressWarnings(); - $success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" ); - wfRestoreWarnings(); - - if( ! $success ) { - $wgOut->showFileRenameError( $this->mSavedFile, - "${archive}/{$this->mUploadOldVersion}" ); - return false; - } - else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n"); - } - else { - $this->mUploadOldVersion = ''; - } - - wfSuppressWarnings(); - $success = $useRename - ? rename( $tempName, $this->mSavedFile ) - : move_uploaded_file( $tempName, $this->mSavedFile ); - wfRestoreWarnings(); - - if( ! $success ) { - $wgOut->showFileCopyError( $tempName, $this->mSavedFile ); + $image = wfLocalFile( $saveName ); + $archiveName = $image->publish( $tempName, File::DELETE_SOURCE ); + if ( WikiError::isError( $archiveName ) ) { + $this->showError( $archiveName ); return false; - } else { - wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n"); } - - chmod( $this->mSavedFile, 0644 ); + $this->mUploadOldVersion = $archiveName; return true; } @@ -593,19 +559,14 @@ class UploadForm { */ function saveTempUploadedFile( $saveName, $tempName ) { global $wgOut; - $archive = wfImageArchiveDir( $saveName, 'temp' ); - if ( !is_dir ( $archive ) ) wfMkdirParents( $archive ); - $stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName; - - $success = $this->mRemoveTempFile - ? rename( $tempName, $stash ) - : move_uploaded_file( $tempName, $stash ); - if ( !$success ) { - $wgOut->showFileCopyError( $tempName, $stash ); + $repo = RepoGroup::singleton()->getLocalRepo(); + $result = $repo->storeTemp( $saveName, $tempName ); + if ( WikiError::isError( $result ) ) { + $this->showError( $result ); return false; + } else { + return $result; } - - return $stash; } /** @@ -662,7 +623,7 @@ class UploadForm { global $wgUser, $wgOut, $wgContLang; $sk = $wgUser->getSkin(); - $ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) ); + $ilink = $sk->makeMediaLinkObj( $this->mImage->getTitle() ); $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName; $dlink = $sk->makeKnownLink( $dname, $dname ); @@ -1274,15 +1235,10 @@ class UploadForm { * @access private */ function checkOverwrite( $name ) { - $img = Image::newFromName( $name ); - if( is_null( $img ) ) { - // Uh... this shouldn't happen ;) - // But if it does, fall through to previous behavior - return false; - } + $img = wfFindFile( $name ); $error = ''; - if( $img->exists() ) { + if( $img ) { global $wgUser, $wgOut; if( $img->isLocal() ) { if( !self::userCanReUpload( $wgUser, $img->name ) ) { @@ -1328,5 +1284,17 @@ class UploadForm { return $user->getID() == $row->img_user; } + + /** + * Display an error from a wikitext-formatted WikiError object + */ + function showError( WikiError $error ) { + global $wgOut; + $wgOut->setPageTitle( wfMsg( "internalerror" ) ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->addWikiText( $error->getMessage() ); + } } ?> diff --git a/includes/StreamFile.php b/includes/StreamFile.php index dc653e57bf..7bda6aeb72 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -31,6 +31,9 @@ function wfStreamFile( $fname ) { header('Content-type: application/x-wiki'); } + global $wgContLanguageCode; + header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) ) ); + if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); $sinceTime = strtotime( $modsince ); diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php new file mode 100644 index 0000000000..fc92b9a152 --- /dev/null +++ b/includes/filerepo/ArchivedFile.php @@ -0,0 +1,112 @@ +getNamespace() == NS_IMAGE ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'filearchive', + array( + 'fa_id', + 'fa_name', + 'fa_storage_key', + 'fa_storage_group', + 'fa_size', + 'fa_bits', + 'fa_width', + 'fa_height', + 'fa_metadata', + 'fa_media_type', + 'fa_major_mime', + 'fa_minor_mime', + 'fa_description', + 'fa_user', + 'fa_user_text', + 'fa_timestamp', + 'fa_deleted' ), + array( + 'fa_name' => $title->getDbKey(), + $conds ), + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + + if ( $dbr->numRows( $res ) == 0 ) { + // this revision does not exist? + return; + } + $ret = $dbr->resultObject( $res ); + $row = $ret->fetchObject(); + + // initialize fields for filestore image object + $this->mId = intval($row->fa_id); + $this->mName = $row->fa_name; + $this->mGroup = $row->fa_storage_group; + $this->mKey = $row->fa_storage_key; + $this->mSize = $row->fa_size; + $this->mBits = $row->fa_bits; + $this->mWidth = $row->fa_width; + $this->mHeight = $row->fa_height; + $this->mMetaData = $row->fa_metadata; + $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime"; + $this->mType = $row->fa_media_type; + $this->mDescription = $row->fa_description; + $this->mUser = $row->fa_user; + $this->mUserText = $row->fa_user_text; + $this->mTimestamp = $row->fa_timestamp; + $this->mDeleted = $row->fa_deleted; + } else { + throw new MWException( 'This title does not correspond to an image page.' ); + return; + } + return true; + } + + /** + * int $field one of DELETED_* bitfield constants + * for file or revision rows + * @return bool + */ + function isDeleted( $field ) { + return ($this->mDeleted & $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 + * @return bool + */ + function userCan( $field ) { + if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) { + // images + global $wgUser; + $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED + ? 'hiderevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } +} + +?> diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php new file mode 100644 index 0000000000..f1eaf29e5f --- /dev/null +++ b/includes/filerepo/FSRepo.php @@ -0,0 +1,368 @@ +name = $info['name']; + $this->directory = $info['directory']; + $this->url = $info['url']; + $this->hashLevels = $info['hashLevels']; + $this->transformVia404 = !empty( $info['transformVia404'] ); + + // Optional settings + foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', + 'thumbScriptUrl' ) as $var ) + { + if ( isset( $info[$var] ) ) { + $this->$var = $info[$var]; + } + } + } + + /** + * Create a new File object from the local repository + * @param mixed $title Title object or string + * @param mixed $time Time at which the image is supposed to have existed. + * If this is specified, the returned object will be an + * instance of the repository's old file class instead of + * a current file. Repositories not supporting version + * control should return false if this parameter is set. + */ + function newFile( $title, $time = false ) { + if ( !($title instanceof Title) ) { + $title = Title::makeTitleSafe( NS_IMAGE, $title ); + if ( !is_object( $title ) ) { + return null; + } + } + if ( $time ) { + return call_user_func( $this->oldFileFactor, $title, $this, $time ); + } else { + return call_user_func( $this->fileFactory, $title, $this ); + } + } + + /** + * Find an instance of the named file that existed at the specified time + * Returns false if the file did not exist. Repositories not supporting + * version control should return false if the time is specified. + * + * @param mixed $time 14-character timestamp, or false for the current version + */ + function findFile( $title, $time = false ) { + $img = $this->newFile( $title ); + if ( !$img ) { + return false; + } + if ( $img->exists() && $img->getTimestamp() <= $time ) { + return $img; + } + $img = $this->newFile( $title, $time ); + if ( $img->exists() ) { + return $img; + } + } + + function getRootDirectory() { + return $this->directory; + } + + function getRootUrl() { + return $this->url; + } + + function isHashed() { + return (bool)$this->hashLevels; + } + + function getThumbScriptUrl() { + return $this->thumbScriptUrl; + } + + function canTransformVia404() { + return $this->transformVia404; + } + + function getZonePath( $zone ) { + switch ( $zone ) { + case 'public': + return $this->directory; + case 'temp': + return "{$this->directory}/temp"; + case 'deleted': + return $GLOBALS['wgFileStore']['deleted']['directory']; + default: + return false; + } + } + + function getZoneUrl( $zone ) { + switch ( $zone ) { + case 'public': + return $this->url; + case 'temp': + return "{$this->url}/temp"; + case 'deleted': + return $GLOBALS['wgFileStore']['deleted']['url']; + default: + return false; + } + } + + /** + * Get a URL referring to this repository, with the private mwrepo protocol. + */ + function getVirtualUrl( $suffix = false ) { + $path = 'mwrepo://'; + if ( $suffix !== false ) { + $path .= '/' . $suffix; + } + return $path; + } + + /** + * Get the local path corresponding to a virtual URL + */ + function resolveVirtualUrl( $url ) { + if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { + throw new MWException( __METHOD__.': unknown protoocl' ); + } + + $bits = explode( '/', substr( $url, 9 ), 3 ); + if ( count( $bits ) != 3 ) { + throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); + } + list( $host, $zone, $rel ) = $bits; + if ( $host !== '' ) { + throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); + } + $base = $this->getZonePath( $zone ); + if ( !$base ) { + throw new MWException( __METHOD__.": invalid zone: $zone" ); + } + return $base . '/' . urldecode( $rel ); + } + + /** + * Store a file to a given destination. + */ + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + $root = $this->getZonePath( $dstZone ); + if ( !$root ) { + throw new MWException( "Invalid zone: $dstZone" ); + } + $dstPath = "$root/$dstRel"; + + if ( !is_dir( dirname( $dstPath ) ) ) { + wfMkdirParents( dirname( $dstPath ) ); + } + + if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { + $srcPath = $this->resolveVirtualUrl( $srcPath ); + } + + if ( $flags & self::DELETE_SOURCE ) { + if ( !rename( $srcPath, $dstPath ) ) { + return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), + wfEscapeWikiText( $dstPath ) ); + } + } else { + if ( !copy( $srcPath, $dstPath ) ) { + return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ), + wfEscapeWikiText( $dstPath ) ); + } + } + chmod( $dstPath, 0644 ); + return true; + } + + /** + * Pick a random name in the temp zone and store a file to it. + * Returns the URL, or a WikiError on failure. + * @param string $originalName The base name of the file as specified + * by the user. The file extension will be maintained. + * @param string $srcPath The current location of the file. + */ + function storeTemp( $originalName, $srcPath ) { + $dstRel = $this->getHashPath( $originalName ) . + gmdate( "YmdHis" ) . '!' . $originalName; + $result = $this->store( $srcPath, 'temp', $dstRel ); + if ( WikiError::isError( $result ) ) { + return $result; + } else { + return $this->getVirtualUrl( "temp/$dstRel" ); + } + } + + function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) { + if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { + $srcPath = $this->resolveVirtualUrl( $srcPath ); + } + $dstDir = dirname( $dstPath ); + if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir ); + + if( is_file( $dstPath ) ) { + $archiveDir = dirname( $archivePath ); + if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir ); + wfSuppressWarnings(); + $success = rename( $dstPath, $archivePath ); + wfRestoreWarnings(); + + if( ! $success ) { + return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ), + wfEscapeWikiText( $archivePath ) ); + } + else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n"); + $status = 'archived'; + } + else { + $status = 'new'; + } + + $error = false; + wfSuppressWarnings(); + if ( $flags & self::DELETE_SOURCE ) { + if ( !rename( $srcPath, $dstPath ) ) { + $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), + wfEscapeWikiText( $dstPath ) ); + } + } else { + if ( !copy( $srcPath, $dstPath ) ) { + $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), + wfEscapeWikiText( $dstPath ) ); + } + } + wfRestoreWarnings(); + + if( $error ) { + return $error; + } else { + wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n"); + } + + chmod( $dstPath, 0644 ); + return $status; + } + + /** + * Get a relative path including trailing slash, e.g. f/fa/ + * If the repo is not hashed, returns an empty string + */ + function getHashPath( $name ) { + if ( $this->isHashed() ) { + $hash = md5( $name ); + $path = ''; + for ( $i = 1; $i <= $this->hashLevels; $i++ ) { + $path .= substr( $hash, 0, $i ) . '/'; + } + return $path; + } else { + return ''; + } + } + + function getName() { + return $this->name; + } + + /** + * Get the file description page base URL, or false if there isn't one. + * @private + */ + function getDescBaseUrl() { + if ( is_null( $this->descBaseUrl ) ) { + if ( !is_null( $this->articleUrl ) ) { + $this->descBaseUrl = str_replace( '$1', + urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); + } elseif ( !is_null( $this->scriptDirUrl ) ) { + $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . + urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':'; + } else { + $this->descBaseUrl = false; + } + } + return $this->descBaseUrl; + } + + /** + * Get the URL of an image description page. May return false if it is + * unknown or not applicable. In general this should only be called by the + * File class, since it may return invalid results for certain kinds of + * repositories. Use File::getDescriptionUrl() in user code. + * + * In particular, it uses the article paths as specified to the repository + * constructor, whereas local repositories use the local Title functions. + */ + function getDescriptionUrl( $name ) { + $base = $this->getDescBaseUrl(); + if ( $base ) { + return $base . wfUrlencode( $name ); + } else { + return false; + } + } + + /** + * Get the URL of the content-only fragment of the description page. For + * MediaWiki this means action=render. This should only be called by the + * repository's file class, since it may return invalid results. User code + * should use File::getDescriptionText(). + */ + function getDescriptionRenderUrl( $name ) { + if ( isset( $this->scriptDirUrl ) ) { + return $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . + '&action=render'; + } else { + $descBase = $this->getDescBaseUrl(); + if ( $descBase ) { + return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' ); + } else { + return false; + } + } + } + + /** + * Call a callback function for every file in the repository. + * Uses the filesystem even in child classes. + */ + function enumFilesInFS( $callback ) { + $numDirs = 1 << ( $this->hashLevels * 4 ); + for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { + $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); + $path = $this->directory; + for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { + $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); + } + if ( !file_exists( $path ) || !is_dir( $path ) ) { + continue; + } + $dir = opendir( $path ); + while ( false !== ( $name = readdir( $dir ) ) ) { + call_user_func( $callback, $path . '/' . $name ); + } + } + } + + /** + * Call a callaback function for every file in the repository + * May use either the database or the filesystem + */ + function enumFiles( $callback ) { + $this->enumFilesInFS( $callback ); + } +} + +?> diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php new file mode 100644 index 0000000000..1e59d2f565 --- /dev/null +++ b/includes/filerepo/File.php @@ -0,0 +1,985 @@ +title = $title; + $this->repo = $repo; + } + + function __get( $name ) { + $function = array( $this, 'get' . ucfirst( $name ) ); + if ( !is_callable( $function ) ) { + return null; + } else { + $this->$name = call_user_func( $function ); + return $this->$name; + } + } + + /** + * Normalize a file extension to the common form, and ensure it's clean. + * Extensions with non-alphanumeric characters will be discarded. + * + * @param $ext string (without the .) + * @return string + */ + static function normalizeExtension( $ext ) { + $lower = strtolower( $ext ); + $squish = array( + 'htm' => 'html', + 'jpeg' => 'jpg', + 'mpeg' => 'mpg', + 'tiff' => 'tif' ); + if( isset( $squish[$lower] ) ) { + return $squish[$lower]; + } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) { + return $lower; + } else { + return ''; + } + } + + /** + * Upgrade the database row if there is one + * Called by ImagePage + * STUB + */ + function upgradeRow() {} + + /** + * Split an internet media type into its two components; if not + * a two-part name, set the minor type to 'unknown'. + * + * @param $mime "text/html" etc + * @return array ("text", "html") etc + */ + static function splitMime( $mime ) { + if( strpos( $mime, '/' ) !== false ) { + return explode( '/', $mime, 2 ); + } else { + return array( $mime, 'unknown' ); + } + } + + /** + * Return the name of this file + * @public + */ + function getName() { + if ( !isset( $this->name ) ) { + $this->name = $this->title->getDBkey(); + } + return $this->name; + } + + /** + * Get the file extension, e.g. "svg" + */ + function getExtension() { + if ( !isset( $this->extension ) ) { + $n = strrpos( $this->getName(), '.' ); + $this->extension = self::normalizeExtension( + $n ? substr( $this->getName(), $n + 1 ) : '' ); + } + return $this->extension; + } + + /** + * Return the associated title object + * @public + */ + function getTitle() { return $this->title; } + + /** + * Return the URL of the file + * @public + */ + function getUrl() { + if ( !isset( $this->url ) ) { + $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel(); + } + return $this->url; + } + + function getViewURL() { + if( $this->mustRender()) { + if( $this->canRender() ) { + return $this->createThumb( $this->getWidth() ); + } + else { + wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n"); + return $this->getURL(); #hm... return NULL? + } + } else { + return $this->getURL(); + } + } + + /** + * Return the full filesystem path to the file. Note that this does + * not mean that a file actually exists under that location. + * + * This path depends on whether directory hashing is active or not, + * i.e. whether the files are all found in the same directory, + * or in hashed paths like /images/3/3c. + * + * May return false if the file is not locally accessible. + * + * @public + */ + function getPath() { + if ( !isset( $this->path ) ) { + $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel(); + } + return $this->path; + } + + /** + * Alias for getPath() + * @public + */ + function getFullPath() { + return $this->getPath(); + } + + /** + * Return the width of the image. Returns false if the width is unknown + * or undefined. + * + * STUB + * Overridden by LocalFile, UnregisteredLocalFile + * @public + */ + function getWidth( $page = 1 ) { return false; } + + /** + * Return the height of the image. Returns false if the height is unknown + * or undefined + * + * STUB + * Overridden by LocalFile, UnregisteredLocalFile + * @public + */ + function getHeight( $page = 1 ) { return false; } + + /** + * Get handler-specific metadata + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + */ + function getMetadata() { return false; } + + /** + * Return the size of the image file, in bytes + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + * @public + */ + function getSize() { return false; } + + /** + * Returns the mime type of the file. + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + */ + function getMimeType() { return 'unknown/unknown'; } + + /** + * Return the type of the media in the file. + * Use the value returned by this function with the MEDIATYPE_xxx constants. + * Overridden by LocalFile, + * STUB + */ + function getMediaType() { return MEDIATYPE_UNKNOWN; } + + /** + * Checks if the file can be presented to the browser as a bitmap. + * + * Currently, this checks if the file is an image format + * that can be converted to a format + * supported by all browsers (namely GIF, PNG and JPEG), + * or if it is an SVG image and SVG conversion is enabled. + */ + function canRender() { + if ( !isset( $this->canRender ) ) { + $this->canRender = $this->getHandler() && $this->handler->canRender(); + } + return $this->canRender; + } + + /** + * Accessor for __get() + */ + protected function getCanRender() { + return $this->canRender(); + } + + /** + * Return true if the file is of a type that can't be directly + * rendered by typical browsers and needs to be re-rasterized. + * + * This returns true for everything but the bitmap types + * supported by all browsers, i.e. JPEG; GIF and PNG. It will + * also return true for any non-image formats. + * + * @return bool + */ + function mustRender() { + return $this->getHandler() && $this->handler->mustRender(); + } + + /** + * Determines if this media file may be shown inline on a page. + * + * This is currently synonymous to canRender(), but this could be + * extended to also allow inline display of other media, + * like flash animations or videos. If you do so, please keep in mind that + * that could be a security risk. + */ + function allowInlineDisplay() { + return $this->canRender(); + } + + /** + * Determines if this media file is in a format that is unlikely to + * contain viruses or malicious content. It uses the global + * $wgTrustedMediaFormats list to determine if the file is safe. + * + * This is used to show a warning on the description page of non-safe files. + * It may also be used to disallow direct [[media:...]] links to such files. + * + * Note that this function will always return true if allowInlineDisplay() + * or isTrustedFile() is true for this file. + */ + function isSafeFile() { + if ( !isset( $this->isSafeFile ) ) { + $this->isSafeFile = $this->_getIsSafeFile(); + } + return $this->isSafeFile; + } + + /** Accessor for __get() */ + protected function getIsSafeFile() { + return $this->isSafeFile(); + } + + /** Uncached accessor */ + protected function _getIsSafeFile() { + if ($this->allowInlineDisplay()) return true; + if ($this->isTrustedFile()) return true; + + global $wgTrustedMediaFormats; + + $type= $this->getMediaType(); + $mime= $this->getMimeType(); + #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n"); + + if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted + if ( in_array( $type, $wgTrustedMediaFormats) ) return true; + + if ($mime==="unknown/unknown") return false; #unknown type, not trusted + if ( in_array( $mime, $wgTrustedMediaFormats) ) return true; + + return false; + } + + /** Returns true if the file is flagged as trusted. Files flagged that way + * can be linked to directly, even if that is not allowed for this type of + * file normally. + * + * This is a dummy function right now and always returns false. It could be + * implemented to extract a flag from the database. The trusted flag could be + * set on upload, if the user has sufficient privileges, to bypass script- + * and html-filters. It may even be coupled with cryptographics signatures + * or such. + */ + function isTrustedFile() { + #this could be implemented to check a flag in the databas, + #look for signatures, etc + return false; + } + + /** + * Returns true if file exists in the repository. + * + * Overridden by LocalFile to avoid unnecessary stat calls. + * + * @return boolean Whether file exists in the repository. + * @public + */ + function exists() { + return $this->getPath() && file_exists( $this->path ); + } + + function getTransformScript() { + if ( !isset( $this->transformScript ) ) { + $this->transformScript = false; + if ( $this->repo ) { + $script = $this->repo->getThumbScriptUrl(); + if ( $script ) { + $this->transformScript = "$script?f=" . urlencode( $this->getName() ); + } + } + } + return $this->transformScript; + } + + /** + * Get a ThumbnailImage which is the same size as the source + */ + function getUnscaledThumb( $page = false ) { + $width = $this->getWidth( $page ); + if ( !$width ) { + return $this->iconThumb(); + } + if ( $page ) { + $params = array( + 'page' => $page, + 'width' => $this->getWidth( $page ) + ); + } else { + $params = array( 'width' => $this->getWidth() ); + } + return $this->transform( $params ); + } + + /** + * Return the file name of a thumbnail with the specified parameters + * + * @param array $params Handler-specific parameters + * @private + */ + function thumbName( $params ) { + if ( !$this->getHandler() ) { + return null; + } + $extension = $this->getExtension(); + list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() ); + $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName(); + if ( $thumbExt != $extension ) { + $thumbName .= ".$thumbExt"; + } + return $thumbName; + } + + /** + * Create a thumbnail of the image having the specified width/height. + * The thumbnail will not be created if the width is larger than the + * image's width. Let the browser do the scaling in this case. + * The thumbnail is stored on disk and is only computed if the thumbnail + * file does not exist OR if it is older than the image. + * Returns the URL. + * + * Keeps aspect ratio of original image. If both width and height are + * specified, the generated image will be no bigger than width x height, + * and will also have correct aspect ratio. + * + * @param integer $width maximum width of the generated thumbnail + * @param integer $height maximum height of the image (optional) + * @public + */ + function createThumb( $width, $height = -1 ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $thumb = $this->transform( $params ); + if( is_null( $thumb ) || $thumb->isError() ) return ''; + return $thumb->getUrl(); + } + + /** + * As createThumb, but returns a ThumbnailImage object. This can + * provide access to the actual file, the real size of the thumb, + * and can produce a convenient tag for you. + * + * For non-image formats, this may return a filetype-specific icon. + * + * @param integer $width maximum width of the generated thumbnail + * @param integer $height maximum height of the image (optional) + * @param boolean $render True to render the thumbnail if it doesn't exist, + * false to just return the URL + * + * @return ThumbnailImage or null on failure + * @public + * + * @deprecated use transform() + */ + function getThumbnail( $width, $height=-1, $render = true ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $flags = $render ? self::RENDER_NOW : 0; + return $this->transform( $params, $flags ); + } + + /** + * Transform a media file + * + * @param array $params An associative array of handler-specific parameters. Typical + * keys are width, height and page. + * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering + * @return MediaTransformOutput + */ + function transform( $params, $flags = 0 ) { + global $wgUseSquid, $wgIgnoreImageErrors; + + wfProfileIn( __METHOD__ ); + do { + if ( !$this->getHandler() || !$this->handler->canRender() ) { + // not a bitmap or renderable image, don't try. + $thumb = $this->iconThumb(); + break; + } + + $script = $this->getTransformScript(); + if ( $script && !($flags & self::RENDER_NOW) ) { + // Use a script to transform on client request + $thumb = $this->handler->getScriptedTransform( $this, $script, $params ); + break; + } + + $normalisedParams = $params; + $this->handler->normaliseParams( $this, $normalisedParams ); + $thumbName = $this->thumbName( $normalisedParams ); + $thumbPath = $this->getThumbPath( $thumbName ); + $thumbUrl = $this->getThumbUrl( $thumbName ); + + if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + + wfDebug( "Doing stat for $thumbPath\n" ); + $this->migrateThumbFile( $thumbName ); + if ( file_exists( $thumbPath ) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); + + // Ignore errors if requested + if ( !$thumb ) { + $thumb = null; + } elseif ( $thumb->isError() ) { + $this->lastError = $thumb->toText(); + if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + } + } + + if ( $wgUseSquid ) { + wfPurgeSquidServers( array( $thumbUrl ) ); + } + } while (false); + + wfProfileOut( __METHOD__ ); + return $thumb; + } + + /** + * Hook into transform() to allow migration of thumbnail files + * STUB + * Overridden by LocalFile + */ + function migrateThumbFile() {} + + /** + * Get a MediaHandler instance for this file + */ + function getHandler() { + if ( !isset( $this->handler ) ) { + $this->handler = MediaHandler::getHandler( $this->getMimeType() ); + } + return $this->handler; + } + + /** + * Get a ThumbnailImage representing a file type icon + * @return ThumbnailImage + */ + function iconThumb() { + global $wgStylePath, $wgStyleDirectory; + + $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ); + foreach( $try as $icon ) { + $path = '/common/images/icons/' . $icon; + $filepath = $wgStyleDirectory . $path; + if( file_exists( $filepath ) ) { + return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); + } + } + return null; + } + + /** + * Get last thumbnailing error. + * Largely obsolete. + */ + function getLastError() { + return $this->lastError; + } + + /** + * Get all thumbnail names previously generated for this file + * STUB + * Overridden by LocalFile + */ + function getThumbnails() { return array(); } + + /** + * Purge shared caches such as thumbnails and DB data caching + * STUB + * Overridden by LocalFile + */ + function purgeCache( $archiveFiles = array() ) {} + + /** + * Purge the file description page, but don't go after + * pages using the file. Use when modifying file history + * but not the current data. + */ + function purgeDescription() { + $title = $this->getTitle(); + if ( $title ) { + $title->invalidateCache(); + $title->purgeSquid(); + } + } + + /** + * Purge metadata and all affected pages when the file is created, + * deleted, or majorly updated. A set of additional URLs may be + * passed to purge, such as specific file files which have changed. + * @param $urlArray array + */ + function purgeEverything( $urlArr=array() ) { + // Delete thumbnails and refresh file metadata cache + $this->purgeCache(); + $this->purgeDescription(); + + // Purge cache of all pages using this file + $title = $this->getTitle(); + if ( $title ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } + } + + /** + * Return the history of this file, line by line. Starts with current version, + * then old versions. Should return an object similar to an image/oldimage + * database row. + * + * @public + * STUB + * Overridden in LocalFile + */ + function nextHistoryLine() { + return false; + } + + /** + * Reset the history pointer to the first element of the history + * @public + * STUB + * Overridden in LocalFile. + */ + function resetHistory() {} + + /** + * Get the filename hash component of the directory including trailing slash, + * e.g. f/fa/ + * If the repository is not hashed, returns an empty string. + */ + function getHashPath() { + if ( !isset( $this->hashPath ) ) { + $this->hashPath = $this->repo->getHashPath( $this->getName() ); + } + return $this->hashPath; + } + + /** + * Get the path of the file relative to the public zone root + */ + function getRel() { + return $this->getHashPath() . $this->getName(); + } + + /** + * Get urlencoded relative path of the file + */ + function getUrlRel() { + return $this->getHashPath() . urlencode( $this->getName() ); + } + + /** Get the path of the archive directory, or a particular file if $suffix is specified */ + function getArchivePath( $suffix = false ) { + $path = $this->repo->getZonePath('public') . '/archive/' . $this->getHashPath(); + if ( $suffix !== false ) { + $path .= '/' . $suffix; + } + return $path; + } + + /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ + function getThumbPath( $suffix = false ) { + $path = $this->repo->getZonePath('public') . '/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 */ + function getArchiveUrl( $suffix = false ) { + $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath(); + if ( $suffix !== false ) { + $path .= '/' . urlencode( $suffix ); + } + return $path; + } + + /** 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(); + if ( $suffix !== false ) { + $path .= '/' . urlencode( $suffix ); + } + return $path; + } + + /** Get the virtual URL for an archive file or directory */ + function getArchiveVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath(); + if ( $suffix !== false ) { + $path .= '/' . urlencode( $suffix ); + } + return $path; + } + + /** Get the virtual URL for a thumbnail file or directory */ + function getThumbVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getHashPath(); + if ( $suffix !== false ) { + $path .= '/' . urlencode( $suffix ); + } + return $path; + } + + /** + * @return bool + */ + function isHashed() { + return $this->repo->isHashed(); + } + + function readOnlyError() { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + + /** + * Record a file upload in the upload log and the image table + * STUB + * Overridden by LocalFile + */ + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { + $this->readOnlyError(); + } + + /** + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. + * + * The archive name should be passed through to recordUpload for database + * registration. + * + * @param string $sourcePath Local filesystem path to the source image + * @param integer $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move + * rather than copy + * @return The archive name on success or an empty string if it was a new + * file, and a wikitext-formatted WikiError object on failure. + * + * STUB + * Overridden by LocalFile + */ + function publish( $srcPath, $flags = 0 ) { + $this->readOnlyError(); + } + + /** + * Get an array of Title objects which are articles which use this file + * Also adds their IDs to the link cache + * + * This is mostly copied from Title::getLinksTo() + * + * @deprecated Use HTMLCacheUpdate, this function uses too much memory + */ + function getLinksTo( $options = '' ) { + wfProfileIn( __METHOD__ ); + + // Note: use local DB not repo DB, we want to know local links + if ( $options ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_SLAVE ); + } + $linkCache =& LinkCache::singleton(); + + list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' ); + $encName = $db->addQuotes( $this->getName() ); + $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; + $res = $db->query( $sql, __METHOD__ ); + + $retVal = array(); + if ( $db->numRows( $res ) ) { + while ( $row = $db->fetchObject( $res ) ) { + if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) { + $linkCache->addGoodLinkObj( $row->page_id, $titleObj ); + $retVal[] = $titleObj; + } + } + } + $db->freeResult( $res ); + wfProfileOut( __METHOD__ ); + return $retVal; + } + + function getExifData() { + if ( !$this->getHandler() || $this->handler->getMetadataType( $this ) != 'exif' ) { + return array(); + } + $metadata = $this->getMetadata(); + if ( !$metadata ) { + return array(); + } + $exif = unserialize( $metadata ); + if ( !$exif ) { + return array(); + } + unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); + $format = new FormatExif( $exif ); + + return $format->getFormattedData(); + } + + /** + * Returns true if the file comes from the local file repository. + * + * @return bool + */ + function isLocal() { + return $this->repo && $this->repo->getName() == 'local'; + } + + /** + * Returns true if the image is an old version + * STUB + */ + function isOld() { + return false; + } + + /** + * Is this file a "deleted" file in a private archive? + * STUB + */ + function isDeleted( $field ) { + return false; + } + + /** + * Was this file ever deleted from the wiki? + * + * @return bool + */ + function wasDeleted() { + $title = $this->getTitle(); + return $title && $title->isDeleted() > 0; + } + + /** + * Delete all versions of the file. + * + * Moves the files into an archive directory (or deletes them) + * and removes the database rows. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @return true on success, false on some kind of failure + * STUB + * Overridden by LocalFile + */ + function delete( $reason, $suppress=false ) { + $this->readOnlyError(); + } + + /** + * Restore all or specified deleted revisions to the given file. + * Permissions and logging are left to the caller. + * + * May throw database exceptions on error. + * + * @param $versions set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @return the number of file revisions restored if successful, + * or false on failure + * STUB + * Overridden by LocalFile + */ + function restore( $versions=array(), $Unsuppress=false ) { + $this->readOnlyError(); + } + + /** + * Returns 'true' if this image is a multipage document, e.g. a DJVU + * document. + * + * @return Bool + */ + function isMultipage() { + return $this->getHandler() && $this->handler->isMultiPage(); + } + + /** + * Returns the number of pages of a multipage document, or NULL for + * documents which aren't multipage documents + */ + function pageCount() { + if ( !isset( $this->pageCount ) ) { + if ( $this->getHandler() && $this->handler->isMultiPage() ) { + $this->pageCount = $this->handler->pageCount( $this ); + } else { + $this->pageCount = false; + } + } + return $this->pageCount; + } + + /** + * Calculate the height of a thumbnail using the source and destination width + */ + static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { + // Exact integer multiply followed by division + if ( $srcWidth == 0 ) { + return 0; + } else { + return round( $srcHeight * $dstWidth / $srcWidth ); + } + } + + /** + * Get an image size array like that returned by getimagesize(), or false if it + * can't be determined. + * + * @param string $fileName The filename + * @return array + */ + function getImageSize( $fileName ) { + if ( !$this->getHandler() ) { + return false; + } + return $this->handler->getImageSize( $this, $fileName ); + } + + /** + * Get the URL of the image description page. May return false if it is + * unknown or not applicable. + */ + function getDescriptionUrl() { + return $this->repo->getDescriptionUrl( $this->getName() ); + } + + /** + * Get the HTML text of the description page, if available + */ + function getDescriptionText() { + if ( !$this->repo->fetchDescription ) { + return false; + } + $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() ); + if ( $renderUrl ) { + wfDebug( "Fetching shared description from $renderUrl\n" ); + return Http::get( $renderUrl ); + } else { + return false; + } + } + + /** + * Get the 14-character timestamp of the file upload, or false if + */ + function getTimestmap() { + $path = $this->getPath(); + if ( !file_exists( $path ) ) { + return false; + } + return wfTimestamp( filemtime( $path ) ); + } + + /** + * Determine if the current user is allowed to view a particular + * field of this file, if it's marked as deleted. + * STUB + * @param int $field + * @return bool + */ + function userCan( $field ) { + return true; + } +} + +?> diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php new file mode 100644 index 0000000000..6c903e9f5c --- /dev/null +++ b/includes/filerepo/ForeignDBFile.php @@ -0,0 +1,39 @@ +repo->hasSharedCache ) { + $hashedName = md5($this->name); + return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix, + 'file', $hashedName ); + } else { + return false; + } + } + + function publish( /*...*/ ) { + $this->readOnlyError(); + } + + function recordUpload( /*...*/ ) { + $this->readOnlyError(); + } + function restore( /*...*/ ) { + $this->readOnlyError(); + } + + function getDescriptionUrl() { + // Restore remote behaviour + return File::getDescriptionUrl(); + } + + function getDescriptionText() { + // Restore remote behaviour + return File::getDescriptionText(); + } +} +?> diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php new file mode 100644 index 0000000000..fc3c829cbd --- /dev/null +++ b/includes/filerepo/ForeignDBRepo.php @@ -0,0 +1,51 @@ +dbType = $info['dbType']; + $this->dbServer = $info['dbServer']; + $this->dbUser = $info['dbUser']; + $this->dbPassword = $info['dbPassword']; + $this->dbName = $info['dbName']; + $this->dbFlags = $info['dbFlags']; + $this->tablePrefix = $info['tablePrefix']; + $this->hasSharedCache = $info['hasSharedCache']; + } + + function getMasterDB() { + if ( !isset( $this->dbConn ) ) { + $class = 'Database' . ucfirst( $this->dbType ); + $this->dbConn = new $class( $this->dbServer, $this->dbUser, + $this->dbPassword, $this->dbName, false, $this->dbFlags, + $this->tablePrefix ); + } + return $this->dbConn; + } + + function getSlaveDB() { + return $this->getMasterDB(); + } + + function hasSharedCache() { + return $this->hasSharedCache; + } + + function store( /*...*/ ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } +} + +?> diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php new file mode 100644 index 0000000000..3a70fd7603 --- /dev/null +++ b/includes/filerepo/LocalFile.php @@ -0,0 +1,1331 @@ +img_name ); + $file = new self( $title, $repo ); + $file->loadFromRow( $row ); + return $file; + } + + function __construct( $title, $repo ) { + if( !is_object( $title ) ) { + throw new MWException( __CLASS__.' constructor given bogus title.' ); + } + parent::__construct( $title, $repo ); + $this->metadata = ''; + $this->historyLine = 0; + $this->dataLoaded = false; + } + + /** + * Get the memcached key + */ + function getCacheKey() { + $hashedName = md5($this->getName()); + return wfMemcKey( 'file', $hashedName ); + } + + /** + * Try to load file metadata from memcached. Returns true on success. + */ + function loadFromCache() { + global $wgMemc; + wfProfileIn( __METHOD__ ); + $this->dataLoaded = false; + $key = $this->getCacheKey(); + if ( !$key ) { + 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 ) ) { + wfDebug( "Pulling file metadata from cache key $key\n" ); + $this->fileExists = $cachedValues['fileExists']; + if ( $this->fileExists ) { + unset( $cachedValues['version'] ); + unset( $cachedValues['fileExists'] ); + foreach ( $cachedValues as $name => $value ) { + $this->$name = $value; + } + } + } + if ( $this->dataLoaded ) { + wfIncrStats( 'image_cache_hit' ); + } else { + wfIncrStats( 'image_cache_miss' ); + } + + wfProfileOut( __METHOD__ ); + return $this->dataLoaded; + } + + /** + * Save the file metadata to memcached + */ + function saveToCache() { + global $wgMemc; + $this->load(); + $key = $this->getCacheKey(); + if ( !$key ) { + return; + } + $fields = $this->getCacheFields( '' ); + $cache = array( 'version' => MW_FILE_VERSION ); + $cache['fileExists'] = $this->fileExists; + if ( $this->fileExists ) { + foreach ( $fields as $field ) { + $cache[$field] = $this->$field; + } + } + + $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week + } + + /** + * Load metadata from the file itself + */ + function loadFromFile() { + wfProfileIn( __METHOD__ ); + $path = $this->getPath(); + $this->fileExists = file_exists( $path ); + $gis = array(); + + if ( $this->fileExists ) { + $magic=& MimeMagic::singleton(); + + $this->mime = $magic->guessMimeType($path,true); + list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime ); + $this->media_type = $magic->getMediaType($path,$this->mime); + $handler = MediaHandler::getHandler( $this->mime ); + + # Get size in bytes + $this->size = filesize( $path ); + + # Height, width and metadata + if ( $handler ) { + $gis = $handler->getImageSize( $this, $path ); + $this->metadata = $handler->getMetadata( $this, $path ); + } else { + $gis = false; + $this->metadata = ''; + } + + wfDebug(__METHOD__.": $path loaded, {$this->size} bytes, {$this->mime}.\n"); + } else { + $this->mime = NULL; + $this->media_type = MEDIATYPE_UNKNOWN; + $this->metadata = ''; + wfDebug(__METHOD__.": $path NOT FOUND!\n"); + } + + if( $gis ) { + $this->width = $gis[0]; + $this->height = $gis[1]; + } else { + $this->width = 0; + $this->height = 0; + } + + #NOTE: $gis[2] contains a code for the image type. This is no longer used. + + #NOTE: we have to set this flag early to avoid load() to be called + # be some of the functions below. This may lead to recursion or other bad things! + # as ther's only one thread of execution, this should be safe anyway. + $this->dataLoaded = true; + + if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; + else $this->bits = 0; + + wfProfileOut( __METHOD__ ); + } + + function getCacheFields( $prefix = 'img_' ) { + static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', + 'major_mime', 'minor_mime', 'metadata', 'timestamp' ); + static $results = array(); + if ( $prefix == '' ) { + return $fields; + } + if ( !isset( $results[$prefix] ) ) { + $prefixedFields = array(); + foreach ( $fields as $field ) { + $prefixedFields[] = $prefix . $field; + } + $results[$prefix] = $prefixedFields; + } + return $results[$prefix]; + } + + /** + * Load file metadata from the DB + */ + function loadFromDB() { + wfProfileIn( __METHOD__ ); + + # Unconditionally set loaded=true, we don't want the accessors constantly rechecking + $this->dataLoaded = true; + + $dbr = $this->repo->getSlaveDB(); + + $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), + array( 'img_name' => $this->getName() ), __METHOD__ ); + if ( $row ) { + $this->loadFromRow( $row ); + } else { + $this->fileExists = false; + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Decode a row from the database (either object or array) to an array + * with timestamps and MIME types decoded, and the field prefix removed. + */ + function decodeRow( $row, $prefix = 'img_' ) { + $array = (array)$row; + $prefixLength = strlen( $prefix ); + // Sanity check prefix once + if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) { + throw new MWException( __METHOD__. ': incorrect $prefix parameter' ); + } + $decoded = array(); + foreach ( $array as $name => $value ) { + $deprefixedName = substr( $name, $prefixLength ); + $decoded[substr( $name, $prefixLength )] = $value; + } + $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] ); + if ( empty( $decoded['major_mime'] ) ) { + $decoded['mime'] = "unknown/unknown"; + } else { + if (!$decoded['minor_mime']) { + $decoded['minor_mime'] = "unknown"; + } + $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime']; + } + return $decoded; + } + + /* + * Load file metadata from a DB result row + */ + function loadFromRow( $row, $prefix = 'img_' ) { + $array = $this->decodeRow( $row, $prefix ); + foreach ( $array as $name => $value ) { + $this->$name = $value; + } + $this->fileExists = true; + // Check for rows from a previous schema, quietly upgrade them + $this->maybeUpgradeRow(); + } + + /** + * Load file metadata from cache or DB, unless already loaded + */ + function load() { + if ( !$this->dataLoaded ) { + if ( !$this->loadFromCache() ) { + $this->loadFromDB(); + $this->saveToCache(); + } + $this->dataLoaded = true; + } + } + + /** + * Upgrade a row if it needs it + */ + function maybeUpgradeRow() { + if ( wfReadOnly() ) { + return; + } + if ( is_null($this->media_type) || $this->mime == 'image/svg' ) { + $this->upgradeRow(); + $this->upgraded = true; + } else { + $handler = $this->getHandler(); + if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { + $this->upgradeRow(); + $this->upgraded = true; + } + } + } + + function getUpgraded() { + return $this->upgraded; + } + + /** + * Fix assorted version-related problems with the image row by reloading it from the file + */ + function upgradeRow() { + wfProfileIn( __METHOD__ ); + + $this->loadFromFile(); + + $dbw = $this->repo->getMasterDB(); + list( $major, $minor ) = self::splitMime( $this->mime ); + + wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n"); + + $dbw->update( 'image', + array( + 'img_width' => $this->width, + 'img_height' => $this->height, + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_metadata' => $this->metadata, + ), array( 'img_name' => $this->getName() ), + __METHOD__ + ); + $this->saveToCache(); + wfProfileOut( __METHOD__ ); + } + + /** splitMime inherited */ + /** getName inherited */ + /** getTitle inherited */ + /** getURL inherited */ + /** getViewURL inherited */ + /** getPath inherited */ + + /** + * Return the width of the image + * + * Returns false on error + * @public + */ + function getWidth( $page = 1 ) { + $this->load(); + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['width']; + } else { + return false; + } + } else { + return $this->width; + } + } + + /** + * Return the height of the image + * + * Returns false on error + * @public + */ + function getHeight( $page = 1 ) { + $this->load(); + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['height']; + } else { + return false; + } + } else { + return $this->height; + } + } + + /** + * Get handler-specific metadata + */ + function getMetadata() { + $this->load(); + return $this->metadata; + } + + /** + * Return the size of the image file, in bytes + * @public + */ + function getSize() { + $this->load(); + return $this->size; + } + + /** + * Returns the mime type of the file. + */ + function getMimeType() { + $this->load(); + return $this->mime; + } + + /** + * Return the type of the media in the file. + * Use the value returned by this function with the MEDIATYPE_xxx constants. + */ + function getMediaType() { + $this->load(); + return $this->media_type; + } + + /** canRender inherited */ + /** mustRender inherited */ + /** allowInlineDisplay inherited */ + /** isSafeFile inherited */ + /** isTrustedFile inherited */ + + /** + * Returns true if the file file exists on disk. + * @return boolean Whether file file exist on disk. + * @public + */ + function exists() { + $this->load(); + return $this->fileExists; + } + + /** getTransformScript inherited */ + /** getUnscaledThumb inherited */ + /** thumbName inherited */ + /** createThumb inherited */ + /** getThumbnail inherited */ + /** transform inherited */ + + /** + * Fix thumbnail files from 1.4 or before, with extreme prejudice + */ + function migrateThumbFile( $thumbName ) { + $thumbDir = $this->getThumbPath(); + $thumbPath = "$thumbDir/$thumbName"; + if ( is_dir( $thumbPath ) ) { + // Directory where file should be + // 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"; + if ( !file_exists( $broken ) ) { + rename( $thumbPath, $broken ); + break; + } + } + // Doesn't exist anymore + clearstatcache(); + } + if ( is_file( $thumbDir ) ) { + // File where directory should be + unlink( $thumbDir ); + // Doesn't exist anymore + clearstatcache(); + } + } + + /** getHandler inherited */ + /** iconThumb inherited */ + /** getLastError inherited */ + + /** + * Get all thumbnail names previously generated for this file + */ + function getThumbnails() { + if ( $this->isHashed() ) { + $this->load(); + $files = array(); + $dir = $this->getThumbPath(); + + if ( is_dir( $dir ) ) { + $handle = opendir( $dir ); + + if ( $handle ) { + while ( false !== ( $file = readdir($handle) ) ) { + if ( $file{0} != '.' ) { + $files[] = $file; + } + } + closedir( $handle ); + } + } + } else { + $files = array(); + } + + return $files; + } + + /** + * Refresh metadata in memcached, but don't touch thumbnails or squid + */ + function purgeMetadataCache() { + clearstatcache(); + $this->loadFromFile(); + $this->saveToCache(); + } + + /** + * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid + */ + function purgeCache( $archiveFiles = array() ) { + global $wgUseSquid; + + // Refresh metadata cache + $this->purgeMetadataCache(); + + // Delete thumbnails + $files = $this->getThumbnails(); + $dir = $this->getThumbPath(); + $urls = array(); + foreach ( $files as $file ) { + $m = array(); + # Check that the base file name is part of the thumb name + # This is a basic sanity check to avoid erasing unrelated directories + if ( strpos( $file, $this->getName() ) !== false ) { + $url = $this->getThumbUrl( $file ); + $urls[] = $url; + @unlink( "$dir/$file" ); + } + } + + // Purge the squid + if ( $wgUseSquid ) { + $urls[] = $this->getURL(); + foreach ( $archiveFiles as $file ) { + $urls[] = $this->getArchiveUrl( $file ); + } + wfPurgeSquidServers( $urls ); + } + } + + /** purgeDescription inherited */ + /** purgeEverything inherited */ + + /** + * Return the history of this file, line by line. + * starts with current version, then old versions. + * uses $this->historyLine to check which line to return: + * 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() { + $dbr = $this->repo->getSlaveDB(); + + if ( $this->historyLine == 0 ) {// called for the first time, return line from cur + $this->historyRes = $dbr->select( 'image', + array( + 'img_size', + 'img_description', + 'img_user','img_user_text', + 'img_timestamp', + 'img_width', + 'img_height', + "'' AS oi_archive_name" + ), + array( 'img_name' => $this->title->getDBkey() ), + __METHOD__ + ); + if ( 0 == $dbr->numRows( $this->historyRes ) ) { + return FALSE; + } + } else if ( $this->historyLine == 1 ) { + $this->historyRes = $dbr->select( 'oldimage', + array( + 'oi_size AS img_size', + 'oi_description AS img_description', + 'oi_user AS img_user', + 'oi_user_text AS img_user_text', + 'oi_timestamp AS img_timestamp', + 'oi_width as img_width', + 'oi_height as img_height', + 'oi_archive_name' + ), + array( 'oi_name' => $this->title->getDBkey() ), + __METHOD__, + array( 'ORDER BY' => 'oi_timestamp DESC' ) + ); + } + $this->historyLine ++; + + return $dbr->fetchObject( $this->historyRes ); + } + + /** + * Reset the history pointer to the first element of the history + * @public + */ + function resetHistory() { + $this->historyLine = 0; + } + + /** getFullPath inherited */ + /** getHashPath inherited */ + /** getRel inherited */ + /** getUrlRel inherited */ + /** getArchivePath inherited */ + /** getThumbPath inherited */ + /** getArchiveUrl inherited */ + /** getThumbUrl inherited */ + /** getArchiveVirtualUrl inherited */ + /** getThumbVirtualUrl inherited */ + /** isHashed inherited */ + + /** + * Record a file upload in the upload log and the image table + */ + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', + $watch = false, $timestamp = false ) + { + global $wgUser, $wgUseCopyrightUpload; + + $dbw = $this->repo->getMasterDB(); + + // Delete thumbnails and refresh the metadata cache + $this->purgeCache(); + + // Fail now if the file isn't there + if ( !$this->fileExists ) { + wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" ); + return false; + } + + if ( $wgUseCopyrightUpload ) { + if ( $license != '' ) { + $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } + $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" . + '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . + "$licensetxt" . + '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; + } else { + if ( $license != '' ) { + $filedesc = $desc == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n"; + $textdesc = $filedesc . + '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } else { + $textdesc = $desc; + } + } + + if ( $timestamp === false ) { + $timestamp = $dbw->timestamp(); + } + + #split mime type + if (strpos($this->mime,'/')!==false) { + list($major,$minor)= explode('/',$this->mime,2); + } + else { + $major= $this->mime; + $minor= "unknown"; + } + + # Test to see if the row exists using INSERT IGNORE + # This avoids race conditions by locking the row until the commit, and also + # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. + $dbw->insert( 'image', + array( + 'img_name' => $this->getName(), + 'img_size'=> $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_timestamp' => $timestamp, + 'img_description' => $desc, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + ), + __METHOD__, + 'IGNORE' + ); + + if( $dbw->affectedRows() == 0 ) { + # Collision, this is an update of a file + # Insert previous contents into oldimage + $dbw->insertSelect( 'oldimage', 'image', + array( + 'oi_name' => 'img_name', + 'oi_archive_name' => $dbw->addQuotes( $oldver ), + 'oi_size' => 'img_size', + 'oi_width' => 'img_width', + 'oi_height' => 'img_height', + 'oi_bits' => 'img_bits', + 'oi_timestamp' => 'img_timestamp', + 'oi_description' => 'img_description', + 'oi_user' => 'img_user', + 'oi_user_text' => 'img_user_text', + ), array( 'img_name' => $this->getName() ), __METHOD__ + ); + + # Update the current image row + $dbw->update( 'image', + array( /* SET */ + 'img_size' => $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_timestamp' => $timestamp, + 'img_description' => $desc, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + ), array( /* WHERE */ + 'img_name' => $this->getName() + ), __METHOD__ + ); + } else { + # This is a new file + # Update the image count + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + } + + $descTitle = $this->getTitle(); + $article = new Article( $descTitle ); + $minor = false; + $watch = $watch || $wgUser->isWatched( $descTitle ); + $suppressRC = true; // There's already a log entry, so don't double the RC load + + if( $descTitle->exists() ) { + // TODO: insert a null revision into the page history for this update. + if( $watch ) { + $wgUser->addWatch( $descTitle ); + } + + # Invalidate the cache for the description page + $descTitle->invalidateCache(); + $descTitle->purgeSquid(); + } else { + // New file; create the description page. + $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC ); + } + + # Hooks, hooks, the magic of hooks... + wfRunHooks( 'FileUpload', array( $this ) ); + + # Add the log entry + $log = new LogPage( 'upload' ); + $log->addEntry( 'upload', $descTitle, $desc ); + + # 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(); + + # Invalidate cache for all pages using this file + $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); + $update->doUpdate(); + + return true; + } + + /** + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. + * + * The archive name should be passed through to recordUpload for database + * registration. + * + * @param string $sourcePath Local filesystem path to the source image + * @param integer $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move + * rather than copy + * @return The archive name on success or an empty string if it was a new + * file, and a wikitext-formatted WikiError object on failure. + */ + function publish( $srcPath, $flags = 0 ) { + $dstPath = $this->getFullPath(); + $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); + $archivePath = $this->getArchivePath( $archiveName ); + $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; + $status = $this->repo->publish( $srcPath, $dstPath, $archivePath, $flags ); + if ( WikiError::isError( $status ) ) { + return $status; + } elseif ( $status == 'new' ) { + return ''; + } else { + return $archiveName; + } + } + + /** getLinksTo inherited */ + /** getExifData inherited */ + /** isLocal inherited */ + /** wasDeleted inherited */ + + /** + * Delete all versions of the file. + * + * Moves the files into an archive directory (or deletes them) + * and removes the database rows. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @return true on success, false on some kind of failure + */ + function delete( $reason, $suppress=false ) { + $transaction = new FSTransaction(); + $urlArr = array( $this->getURL() ); + + if( !FileStore::lock() ) { + wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); + return false; + } + + try { + $dbw = $this->repo->getMasterDB(); + $dbw->begin(); + + // Delete old versions + $result = $dbw->select( 'oldimage', + array( 'oi_archive_name' ), + array( 'oi_name' => $this->getName() ) ); + + while( $row = $dbw->fetchObject( $result ) ) { + $oldName = $row->oi_archive_name; + + $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) ); + + // We'll need to purge this URL from caches... + $urlArr[] = $this->getArchiveUrl( $oldName ); + } + $dbw->freeResult( $result ); + + // And the current version... + $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) ); + + $dbw->immediateCommit(); + } catch( MWException $e ) { + wfDebug( __METHOD__.": db error, rolling back file transactions\n" ); + $transaction->rollback(); + FileStore::unlock(); + throw $e; + } + + wfDebug( __METHOD__.": deleted db items, applying file transactions\n" ); + $transaction->commit(); + FileStore::unlock(); + + + // Update site_stats + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); + + $this->purgeEverything( $urlArr ); + + return true; + } + + + /** + * Delete an old version of the file. + * + * Moves the file into an archive directory (or deletes it) + * and removes the database row. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @throws MWException or FSException on database or filestore failure + * @return true on success, false on some kind of failure + */ + function deleteOld( $archiveName, $reason, $suppress=false ) { + $transaction = new FSTransaction(); + $urlArr = array(); + + if( !FileStore::lock() ) { + wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); + return false; + } + + $transaction = new FSTransaction(); + try { + $dbw = $this->repo->getMasterDB(); + $dbw->begin(); + $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) ); + $dbw->immediateCommit(); + } catch( MWException $e ) { + wfDebug( __METHOD__.": db error, rolling back file transaction\n" ); + $transaction->rollback(); + FileStore::unlock(); + throw $e; + } + + wfDebug( __METHOD__.": deleted db items, applying file transaction\n" ); + $transaction->commit(); + FileStore::unlock(); + + $this->purgeDescription(); + + // Squid purging + global $wgUseSquid; + if ( $wgUseSquid ) { + $urlArr = array( + $this->getArchiveUrl( $archiveName ), + ); + wfPurgeSquidServers( $urlArr ); + } + return true; + } + + /** + * Delete the current version of a file. + * May throw a database error. + * @return true on success, false on failure + */ + private function prepareDeleteCurrent( $reason, $suppress=false ) { + return $this->prepareDeleteVersion( + $this->getFullPath(), + $reason, + 'image', + array( + 'fa_name' => 'img_name', + 'fa_archive_name' => 'NULL', + 'fa_size' => 'img_size', + 'fa_width' => 'img_width', + 'fa_height' => 'img_height', + 'fa_metadata' => 'img_metadata', + 'fa_bits' => 'img_bits', + 'fa_media_type' => 'img_media_type', + 'fa_major_mime' => 'img_major_mime', + 'fa_minor_mime' => 'img_minor_mime', + 'fa_description' => 'img_description', + 'fa_user' => 'img_user', + 'fa_user_text' => 'img_user_text', + 'fa_timestamp' => 'img_timestamp' ), + array( 'img_name' => $this->getName() ), + $suppress, + __METHOD__ ); + } + + /** + * Delete a given older version of a file. + * May throw a database error. + * @return true on success, false on failure + */ + private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) { + $oldpath = $this->getArchivePath() . + DIRECTORY_SEPARATOR . $archiveName; + return $this->prepareDeleteVersion( + $oldpath, + $reason, + 'oldimage', + array( + 'fa_name' => 'oi_name', + 'fa_archive_name' => 'oi_archive_name', + 'fa_size' => 'oi_size', + 'fa_width' => 'oi_width', + 'fa_height' => 'oi_height', + 'fa_metadata' => 'NULL', + 'fa_bits' => 'oi_bits', + 'fa_media_type' => 'NULL', + 'fa_major_mime' => 'NULL', + 'fa_minor_mime' => 'NULL', + 'fa_description' => 'oi_description', + 'fa_user' => 'oi_user', + 'fa_user_text' => 'oi_user_text', + 'fa_timestamp' => 'oi_timestamp' ), + array( + 'oi_name' => $this->getName(), + 'oi_archive_name' => $archiveName ), + $suppress, + __METHOD__ ); + } + + /** + * Do the dirty work of backing up an image row and its file + * (if $wgSaveDeletedFiles is on) and removing the originals. + * + * Must be run while the file store is locked and a database + * transaction is open to avoid race conditions. + * + * @return FSTransaction + */ + private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) { + global $wgUser, $wgSaveDeletedFiles; + + // Dupe the file into the file store + if( file_exists( $path ) ) { + if( $wgSaveDeletedFiles ) { + $group = 'deleted'; + + $store = FileStore::get( $group ); + $key = FileStore::calculateKey( $path, $this->getExtension() ); + $transaction = $store->insert( $key, $path, + FileStore::DELETE_ORIGINAL ); + } else { + $group = null; + $key = null; + $transaction = FileStore::deleteFile( $path ); + } + } else { + wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" ); + $group = null; + $key = null; + $transaction = new FSTransaction(); // empty + } + + if( $transaction === false ) { + // Fail to restore? + wfDebug( __METHOD__.": import to file store failed, aborting\n" ); + throw new MWException( "Could not archive and delete file $path" ); + return false; + } + + // Bitfields to further supress the file content + // Note that currently, live files are stored elsewhere + // and cannot be partially deleted + $bitfield = 0; + if ( $suppress ) { + $bitfield |= self::DELETED_FILE; + $bitfield |= self::DELETED_COMMENT; + $bitfield |= self::DELETED_USER; + $bitfield |= self::DELETED_RESTRICTED; + } + + $dbw = $this->repo->getMasterDB(); + $storageMap = array( + 'fa_storage_group' => $dbw->addQuotes( $group ), + 'fa_storage_key' => $dbw->addQuotes( $key ), + + 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ), + 'fa_deleted_timestamp' => $dbw->timestamp(), + 'fa_deleted_reason' => $dbw->addQuotes( $reason ), + 'fa_deleted' => $bitfield); + $allFields = array_merge( $storageMap, $fieldMap ); + + try { + if( $wgSaveDeletedFiles ) { + $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname ); + } + $dbw->delete( $table, $where, $fname ); + } catch( DBQueryError $e ) { + // Something went horribly wrong! + // Leave the file as it was... + wfDebug( __METHOD__.": database error, rolling back file transaction\n" ); + $transaction->rollback(); + throw $e; + } + + return $transaction; + } + + /** + * Restore all or specified deleted revisions to the given file. + * Permissions and logging are left to the caller. + * + * May throw database exceptions on error. + * + * @param $versions set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @return the number of file revisions restored if successful, + * or false on failure + */ + function restore( $versions=array(), $Unsuppress=false ) { + global $wgUser; + + if( !FileStore::lock() ) { + wfDebug( __METHOD__." could not acquire filestore lock\n" ); + return false; + } + + $transaction = new FSTransaction(); + try { + $dbw = $this->repo->getMasterDB(); + $dbw->begin(); + + // Re-confirm whether this file presently exists; + // if no we'll need to create an file record for the + // first item we restore. + $exists = $dbw->selectField( 'image', '1', + array( 'img_name' => $this->getName() ), + __METHOD__ ); + + // Fetch all or selected archived revisions for the file, + // sorted from the most recent to the oldest. + $conditions = array( 'fa_name' => $this->getName() ); + if( $versions ) { + $conditions['fa_id'] = $versions; + } + + $result = $dbw->select( 'filearchive', '*', + $conditions, + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + + if( $dbw->numRows( $result ) < count( $versions ) ) { + // There's some kind of conflict or confusion; + // we can't restore everything we were asked to. + wfDebug( __METHOD__.": couldn't find requested items\n" ); + $dbw->rollback(); + FileStore::unlock(); + return false; + } + + if( $dbw->numRows( $result ) == 0 ) { + // Nothing to do. + wfDebug( __METHOD__.": nothing to do\n" ); + $dbw->rollback(); + FileStore::unlock(); + return true; + } + + $revisions = 0; + while( $row = $dbw->fetchObject( $result ) ) { + if ( $Unsuppress ) { + // Currently, fa_deleted flags fall off upon restore, lets be careful about this + } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { + // Skip restoring file revisions that the user cannot restore + continue; + } + $revisions++; + $store = FileStore::get( $row->fa_storage_group ); + if( !$store ) { + wfDebug( __METHOD__.": skipping row with no file.\n" ); + continue; + } + + $restoredImage = new self( $row->fa_name, $this->repo ); + + if( $revisions == 1 && !$exists ) { + $destPath = $restoredImage->getFullPath(); + $destDir = dirname( $destPath ); + if ( !is_dir( $destDir ) ) { + wfMkdirParents( $destDir ); + } + + // We may have to fill in data if this was originally + // an archived file revision. + if( is_null( $row->fa_metadata ) ) { + $tempFile = $store->filePath( $row->fa_storage_key ); + + $magic = MimeMagic::singleton(); + $mime = $magic->guessMimeType( $tempFile, true ); + $media_type = $magic->getMediaType( $tempFile, $mime ); + list( $major_mime, $minor_mime ) = self::splitMime( $mime ); + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + $metadata = $handler->getMetadata( false, $tempFile ); + } else { + $metadata = ''; + } + } else { + $metadata = $row->fa_metadata; + $major_mime = $row->fa_major_mime; + $minor_mime = $row->fa_minor_mime; + $media_type = $row->fa_media_type; + } + + $table = 'image'; + $fields = array( + 'img_name' => $row->fa_name, + 'img_size' => $row->fa_size, + 'img_width' => $row->fa_width, + 'img_height' => $row->fa_height, + 'img_metadata' => $metadata, + 'img_bits' => $row->fa_bits, + 'img_media_type' => $media_type, + 'img_major_mime' => $major_mime, + 'img_minor_mime' => $minor_mime, + 'img_description' => $row->fa_description, + 'img_user' => $row->fa_user, + 'img_user_text' => $row->fa_user_text, + 'img_timestamp' => $row->fa_timestamp ); + } else { + $archiveName = $row->fa_archive_name; + if( $archiveName == '' ) { + // This was originally a current version; we + // have to devise a new archive name for it. + // Format is ! + $archiveName = + wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) . + '!' . $row->fa_name; + } + $restoredImage = new self( $row->fa_name, $this->repo ); + $destDir = $restoredImage->getArchivePath(); + if ( !is_dir( $destDir ) ) { + wfMkdirParents( $destDir ); + } + $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName; + + $table = 'oldimage'; + $fields = array( + 'oi_name' => $row->fa_name, + 'oi_archive_name' => $archiveName, + 'oi_size' => $row->fa_size, + 'oi_width' => $row->fa_width, + 'oi_height' => $row->fa_height, + 'oi_bits' => $row->fa_bits, + 'oi_description' => $row->fa_description, + 'oi_user' => $row->fa_user, + 'oi_user_text' => $row->fa_user_text, + 'oi_timestamp' => $row->fa_timestamp ); + } + + $dbw->insert( $table, $fields, __METHOD__ ); + // @todo this delete is not totally safe, potentially + $dbw->delete( 'filearchive', + array( 'fa_id' => $row->fa_id ), + __METHOD__ ); + + // Check if any other stored revisions use this file; + // if so, we shouldn't remove the file from the deletion + // archives so they will still work. + $useCount = $dbw->selectField( 'filearchive', + 'COUNT(*)', + array( + 'fa_storage_group' => $row->fa_storage_group, + 'fa_storage_key' => $row->fa_storage_key ), + __METHOD__ ); + if( $useCount == 0 ) { + wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" ); + $flags = FileStore::DELETE_ORIGINAL; + } else { + $flags = 0; + } + + $transaction->add( $store->export( $row->fa_storage_key, + $destPath, $flags ) ); + } + + $dbw->immediateCommit(); + } catch( MWException $e ) { + wfDebug( __METHOD__." caught error, aborting\n" ); + $transaction->rollback(); + throw $e; + } + + $transaction->commit(); + FileStore::unlock(); + + if( $revisions > 0 ) { + if( !$exists ) { + wfDebug( __METHOD__." restored $revisions items, creating a new current\n" ); + + // Update site_stats + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + + $this->purgeEverything(); + } else { + wfDebug( __METHOD__." restored $revisions as archived versions\n" ); + $this->purgeDescription(); + } + } + + return $revisions; + } + + /** isMultipage inherited */ + /** pageCount inherited */ + /** scaleHeight inherited */ + /** getImageSize inherited */ + + /** + * Get the URL of the file description page. + */ + function getDescriptionUrl() { + return $this->title->getLocalUrl(); + } + + /** + * Get the HTML text of the description page + * This is not used by ImagePage for local files, since (among other things) + * it skips the parser cache. + */ + function getDescriptionText() { + global $wgParser; + $revision = Revision::newFromTitle( $this->title ); + if ( !$revision ) return false; + $text = $revision->getText(); + if ( !$text ) return false; + $html = $wgParser->parse( $text, new ParserOptions ); + return $html; + } + + function getTimestamp() { + $this->load(); + return $this->timestamp; + } +} // LocalFile class + +/** + * Backwards compatibility class + */ +class Image extends LocalFile { + function __construct( $title ) { + $repo = FileRepoGroup::singleton()->getLocalRepo(); + parent::__construct( $title, $repo ); + } + + /** + * Wrapper for wfFindFile(), for backwards-compatibility only + * Do not use in core code. + */ + function newFromTitle( $title, $time = false ) { + $img = wfFindFile( $title, $time ); + if ( !$img ) { + $img = wfLocalFile( $title ); + } + return $img; + } +} + +/** + * Aliases for backwards compatibility with 1.6 + */ +define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE ); +define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT ); +define( 'MW_IMG_DELETED_USER', File::DELETED_USER ); +define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED ); + +?> diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php new file mode 100644 index 0000000000..62d937d90f --- /dev/null +++ b/includes/filerepo/LocalRepo.php @@ -0,0 +1,26 @@ +img_name ) ) { + return LocalFile::newFromRow( $row, $this ); + } elseif ( isset( $row->oi_name ) ) { + return OldLocalFile::newFromRow( $row, $this ); + } else { + throw new MWException( __METHOD__.': invalid row' ); + } + } +} diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php new file mode 100644 index 0000000000..1a802a0a13 --- /dev/null +++ b/includes/filerepo/OldLocalFile.php @@ -0,0 +1,222 @@ +oi_name ); + $file = new self( $title, $repo, null, $row->oi_archive_name ); + $file->loadFromRow( $row, 'oi_' ); + return $file; + } + + /** + * @param Title $title + * @param FileRepo $repo + * @param string $time Timestamp or null to load by archive name + * @param string $archiveName Archive name or null to load by timestamp + */ + function __construct( $title, $repo, $time, $archiveName ) { + parent::__construct( $title, $repo ); + $this->requestedTime = $time; + $this->archive_name = $archiveName; + if ( is_null( $time ) && is_null( $archiveName ) ) { + throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' ); + } + } + + function getCacheKey() { + $hashedName = md5($this->getName()); + return wfMemcKey( 'oldfile', $hashedName ); + } + + function getArchiveName() { + if ( !isset( $this->archive_name ) ) { + $this->load(); + } + return $this->archive_name; + } + + function isOld() { + return true; + } + + /** + * Try to load file metadata from memcached. Returns true on success. + */ + function loadFromCache() { + global $wgMemc; + wfProfileIn( __METHOD__ ); + $this->dataLoaded = false; + $key = $this->getCacheKey(); + if ( !$key ) { + return false; + } + $oldImages = $wgMemc->get( $key ); + + if ( isset( $oldImages['version'] ) && $oldImages['version'] == MW_OLDFILE_VERSION ) { + unset( $oldImages['version'] ); + $more = isset( $oldImages['more'] ); + unset( $oldImages['more'] ); + $found = false; + if ( is_null( $this->requestedTime ) ) { + foreach ( $oldImages as $timestamp => $info ) { + if ( $info['archive_name'] == $this->archive_name ) { + $found = true; + break; + } + } + } else { + krsort( $oldImages ); + foreach ( $oldImages as $timestamp => $info ) { + if ( $timestamp <= $this->requestedTime ) { + $found = true; + break; + } + } + } + if ( $found ) { + wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" ); + $this->dataLoaded = true; + foreach ( $cachedValues as $name => $value ) { + $this->$name = $value; + } + } elseif ( $more ) { + wfDebug( "Cache key was truncated, oldimage row might be found in the database\n" ); + } else { + wfDebug( "Image did not exist at the specified time.\n" ); + $this->fileExists = false; + $this->dataLoaded = true; + } + } + + if ( $this->dataLoaded ) { + wfIncrStats( 'image_cache_hit' ); + } else { + wfIncrStats( 'image_cache_miss' ); + } + + wfProfileOut( __METHOD__ ); + return $this->dataLoaded; + } + + function saveToCache() { + // Cache the entire history of the image (up to MAX_CACHE_ROWS). + // This is expensive, so we only do it if $wgMemc is real + global $wgMemc; + if ( $wgMemc instanceof FakeMemcachedClient ) { + return; + } + $key = $this->getCacheKey(); + if ( !$key ) { + return; + } + wfProfileIn( __METHOD__ ); + + $dbr = $this->repo->getSlaveDB(); + $res = $dbr->select( 'oldimage', $this->getCacheFields(), + array( 'oi_name' => $this->getName() ), __METHOD__, + array( + 'LIMIT' => self::MAX_CACHE_ROWS + 1, + 'ORDER BY' => 'oi_timestamp DESC', + )); + $cache = array( 'version' => self::CACHE_VERSION ); + $numRows = $dbr->numRows( $res ); + if ( $numRows > self::MAX_CACHE_ROWS ) { + $cache['more'] = true; + $numRows--; + } + for ( $i = 0; $i < $numRows; $i++ ) { + $row = $dbr->fetchObject( $res ); + $this->decodeRow( $row, 'oi_' ); + $cache[$row->oi_timestamp] = $row; + } + $dbr->freeResult( $res ); + $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ ); + wfProfileOut( __METHOD__ ); + } + + function loadFromDB() { + wfProfileIn( __METHOD__ ); + $dbr = $this->repo->getSlaveDB(); + $conds = array( 'oi_name' => $this->getName() ); + if ( is_null( $this->requestedTimestamp ) ) { + $conds['oi_archive_name'] = $this->archive_name; + } else { + $conds[] = 'oi_timestamp <= ' . $dbr->addQuotes( $this->requestedTimestamp ); + } + $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), + $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) ); + if ( $row ) { + $this->loadFromRow( $row, 'oi_' ); + } else { + $this->fileExists = false; + } + $this->dataLoaded = true; + } + + function getCacheFields( $prefix = 'img_' ) { + $fields = parent::getCacheFields( $prefix ); + $fields[] = $prefix . 'archive_name'; + + // XXX: Temporary hack before schema update + $fields = array_diff( $fields, array( + 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) ); + return $fields; + } + + function getRel() { + return 'archive/' . $this->getHashPath() . $this->getArchiveName(); + } + + function getUrlRel() { + return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() ); + } + + function upgradeRow() { + wfProfileIn( __METHOD__ ); + + $this->loadFromFile(); + + $dbw = $this->repo->getMasterDB(); + list( $major, $minor ) = self::splitMime( $this->mime ); + + wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n"); + $dbw->update( 'oldimage', + array( + 'oi_width' => $this->width, + 'oi_height' => $this->height, + 'oi_bits' => $this->bits, + #'oi_media_type' => $this->media_type, + #'oi_major_mime' => $major, + #'oi_minor_mime' => $minor, + #'oi_metadata' => $this->metadata, + ), array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->requestedTime ), + __METHOD__ + ); + wfProfileOut( __METHOD__ ); + } + + // XXX: Temporary hack before schema update + function maybeUpgradeRow() {} + +} + + +?> diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php new file mode 100644 index 0000000000..3055382624 --- /dev/null +++ b/includes/filerepo/RepoGroup.php @@ -0,0 +1,98 @@ +localInfo = $localInfo; + $this->foreignInfo = $foreignInfo; + } + + /** + * Search repositories for an image. + * You can also use wfGetFile() to do this. + * @param mixed $title Title object or string + * @param mixed $time The 14-char timestamp before which the file should + * have been uploaded, or false for the current version + * @return File object or false if it is not found + */ + function findFile( $title, $time = false ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $image = $this->localRepo->findFile( $title, $time ); + if ( $image ) { + return $image; + } + foreach ( $this->foreignRepos as $repo ) { + $image = $repo->findFile( $image, $time ); + if ( $image ) { + return $image; + } + } + return false; + } + + /** + * Get the repo instance with a given key. + */ + function getRepo( $index ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + if ( $index == 'local' ) { + return $this->localRepo; + } elseif ( isset( $this->foreignRepos[$index] ) ) { + return $this->foreignRepos[$index]; + } else { + return false; + } + } + + function getLocalRepo() { + return $this->getRepo( 'local' ); + } + + /** + * Initialise the $repos array + */ + function initialiseRepos() { + if ( $this->reposInitialised ) { + return; + } + $this->reposInitialised = true; + + $this->localRepo = $this->newRepo( $this->localInfo ); + $this->foreignRepos = array(); + foreach ( $this->foreignInfo as $key => $info ) { + $this->foreignRepos[$key] = $this->newRepo( $info ); + } + } + + function newRepo( $info ) { + $class = $info['class']; + return new $class( $info ); + } +} + +?> diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php new file mode 100644 index 0000000000..15dcf00c95 --- /dev/null +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -0,0 +1,109 @@ +title = $title; + $this->name = $title->getDBkey(); + } else { + $this->name = basename( $path ); + $this->title = Title::makeTitleSafe( NS_IMAGE, $this->name ); + } + $this->repo = $repo; + if ( $path ) { + $this->path = $path; + } else { + $this->path = $repo->getRootDirectory() . '/' . $repo->getHashPath( $this->name ) . $this->name; + } + if ( $mime ) { + $this->mime = $mime; + } + $this->dims = array(); + } + + function getPageDimensions( $page = 1 ) { + if ( !isset( $this->dims[$page] ) ) { + if ( !$this->getHandler() ) { + return false; + } + $this->dims[$page] = $this->handler->getPageDimensions( $this, $page ); + } + return $this->dims[$page]; + } + + function getWidth( $page = 1 ) { + $dim = $this->getPageDimensions( $page ); + return $dim['width']; + } + + function getHeight( $page = 1 ) { + $dim = $this->getPageDimensions( $page ); + return $dim['height']; + } + + function getMimeType() { + if ( !isset( $this->mime ) ) { + $magic = MimeMagic::singleton(); + $this->mime = $magic->guessMimeType( $this->path ); + } + return $this->mime; + } + + function getImageSize() { + if ( !$this->getHandler() ) { + return false; + } + return $this->handler->getImageSize( $this, $this->getPath() ); + } + + function getMetadata() { + if ( !isset( $this->metadata ) ) { + if ( !$this->getHandler() ) { + $this->metadata = false; + } else { + $this->metadata = $this->handler->getMetadata( $this, $this->getPath() ); + } + } + return $this->metadata; + } + + function getURL() { + if ( $this->repo ) { + return $this->repo->getZoneUrl( 'public' ) . $this->repo->getHashPath( $this->name ) . urlencode( $this->name ); + } else { + return false; + } + } + + function getSize() { + if ( file_exists( $this->path ) ) { + return filesize( $this->path ); + } else { + return false; + } + } +} +?> diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 8024cb3611..b8ccbd259d 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -51,7 +51,7 @@ class BitmapHandler extends ImageHandler { $srcWidth = $image->getWidth(); $srcHeight = $image->getHeight(); $mimeType = $image->getMimeType(); - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); $retval = 0; wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); @@ -61,7 +61,10 @@ class BitmapHandler extends ImageHandler { return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath ); } - if ( $wgUseImageMagick ) { + if ( !$dstPath ) { + // No output path available, client side scaling only + $scaler = 'client'; + } elseif ( $wgUseImageMagick ) { $scaler = 'im'; } elseif ( $wgCustomConvertCommand ) { $scaler = 'custom'; diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 00a1b516cf..1dab2f05cb 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -69,7 +69,7 @@ class DjVuHandler extends ImageHandler { } $width = $params['width']; $height = $params['height']; - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); $page = $params['page']; if ( $page > $this->pageCount( $image ) ) { return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) ); diff --git a/includes/media/Generic.php b/includes/media/Generic.php index 189045a932..c21ad79187 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -152,7 +152,7 @@ abstract class MediaHandler { * Returns false if unknown or if the document is not multi-page. */ function getPageDimensions( $image, $page ) { - $gis = $this->getImageSize( $image, $image->getImagePath() ); + $gis = $this->getImageSize( $image, $image->getPath() ); return array( 'width' => $gis[0], 'height' => $gis[1] @@ -220,7 +220,7 @@ abstract class ImageHandler extends MediaHandler { $params['width'] = wfFitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); } } - $params['height'] = Image::scaleHeight( $srcWidth, $srcHeight, $params['width'] ); + $params['height'] = File::scaleHeight( $srcWidth, $srcHeight, $params['width'] ); if ( !$this->validateThumbParams( $params['width'], $params['height'], $srcWidth, $srcHeight, $mimeType ) ) { return false; } @@ -254,7 +254,7 @@ abstract class ImageHandler extends MediaHandler { return false; } - $height = Image::scaleHeight( $srcWidth, $srcHeight, $width ); + $height = File::scaleHeight( $srcWidth, $srcHeight, $width ); return true; } diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 2dfbd02e24..03a5560412 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -31,7 +31,7 @@ class SvgHandler extends ImageHandler { $srcWidth = $image->getWidth( $params['page'] ); $srcHeight = $image->getHeight( $params['page'] ); $params['physicalWidth'] = $wgSVGMaxSize; - $params['physicalHeight'] = Image::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize ); + $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize ); } return true; } @@ -46,7 +46,7 @@ class SvgHandler extends ImageHandler { $clientHeight = $params['height']; $physicalWidth = $params['physicalWidth']; $physicalHeight = $params['physicalHeight']; - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); if ( $flags & self::TRANSFORM_LATER ) { return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index 43bbafd864..313fe9a5d9 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -29,59 +29,6 @@ $utfCanonicalDecomp = NULL; global $utfCompatibilityDecomp; $utfCompatibilityDecomp = NULL; -define( 'UNICODE_HANGUL_FIRST', 0xac00 ); -define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); - -define( 'UNICODE_HANGUL_LBASE', 0x1100 ); -define( 'UNICODE_HANGUL_VBASE', 0x1161 ); -define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); - -define( 'UNICODE_HANGUL_LCOUNT', 19 ); -define( 'UNICODE_HANGUL_VCOUNT', 21 ); -define( 'UNICODE_HANGUL_TCOUNT', 28 ); -define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); - -define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); -define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); -define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); - -define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); -define( 'UNICODE_SURROGATE_LAST', 0xdfff ); -define( 'UNICODE_MAX', 0x10ffff ); -define( 'UNICODE_REPLACEMENT', 0xfffd ); - - -define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); -define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); - -define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); -define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); -define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); - -define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); -define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); -define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); - -define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); -define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); -define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); -define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); -#define( 'UTF8_REPLACEMENT', '!' ); - -define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); -define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); -define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); - -# These two ranges are illegal -define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); -define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); -define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); -define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); - -define( 'UTF8_HEAD', false ); -define( 'UTF8_TAIL', true ); - - /** * For using the ICU wrapper */ diff --git a/maintenance/FiveUpgrade.inc b/maintenance/FiveUpgrade.inc index c32f1b2e4b..9a882bc60a 100644 --- a/maintenance/FiveUpgrade.inc +++ b/maintenance/FiveUpgrade.inc @@ -693,10 +693,7 @@ END; return $copy; } - function imageInfo( $name, $subdirCallback='wfImageDir', $basename = null ) { - if( is_null( $basename ) ) $basename = $name; - $dir = call_user_func( $subdirCallback, $basename ); - $filename = $dir . '/' . $name; + function imageInfo( $filename ) { $info = array( 'width' => 0, 'height' => 0, @@ -711,20 +708,13 @@ END; $info['media'] = $magic->getMediaType( $filename, $mime ); - # Height and width - $gis = false; - if( $mime == 'image/svg' ) { - $gis = wfGetSVGsize( $filename ); - } elseif( $magic->isPHPImageType( $mime ) ) { - $gis = getimagesize( $filename ); - } else { - $this->log( "Surprising mime type: $mime" ); - } - if( $gis ) { - $info['width' ] = $gis[0]; - $info['height'] = $gis[1]; - } - if( isset( $gis['bits'] ) ) { + $image = UnregisteredLocalFile::newFromPath( $filename, $mime ); + + $info['width'] = $image->getWidth(); + $info['height'] = $image->getHeight(); + + $gis = $image->getImageSize(); + if ( isset( $gis['bits'] ) ) { $info['bits'] = $gis['bits']; } @@ -896,7 +886,7 @@ END; } function upgradeLogging() { - $tabledef = << MW_UPGRADE_COPY, 'log_action' => MW_UPGRADE_COPY, @@ -940,7 +930,7 @@ END; } function upgradeArchive() { - $tabledef = << MW_UPGRADE_COPY, 'ar_title' => MW_UPGRADE_ENCODE, @@ -979,7 +969,7 @@ END; function upgradeImagelinks() { global $wgUseLatin1; if( $wgUseLatin1 ) { - $tabledef = << MW_UPGRADE_COPY, 'il_to' => MW_UPGRADE_ENCODE ); @@ -1004,7 +994,7 @@ END; function upgradeCategorylinks() { global $wgUseLatin1; if( $wgUseLatin1 ) { - $tabledef = << MW_UPGRADE_COPY, 'cl_to' => MW_UPGRADE_ENCODE, @@ -1028,7 +1018,7 @@ END; function upgradeIpblocks() { global $wgUseLatin1; if( $wgUseLatin1 ) { - $tabledef = << MW_UPGRADE_COPY, 'ipb_address' => MW_UPGRADE_COPY, @@ -1060,7 +1050,7 @@ END; function upgradeRecentchanges() { // There's a format change in the namespace field - $tabledef = << MW_UPGRADE_COPY, 'rc_timestamp' => MW_UPGRADE_COPY, @@ -1124,7 +1114,7 @@ END; function upgradeQuerycache() { // There's a format change in the namespace field - $tabledef = << MW_UPGRADE_COPY, 'qc_value' => MW_UPGRADE_COPY, diff --git a/maintenance/cleanupImages.php b/maintenance/cleanupImages.php index 3ec2c44357..156e06b804 100644 --- a/maintenance/cleanupImages.php +++ b/maintenance/cleanupImages.php @@ -89,7 +89,10 @@ class ImageCleanup extends TableCleanup { } function filePath( $name ) { - return wfImageDir( $name ) . "/$name"; + if ( !isset( $this->repo ) ) { + $this->repo = RepoGroup::singleton()->getLocalRepo(); + } + return $this->repo->getRootDirectory() . '/' . $this->repo->getHashPath( $name ) . $name; } function pokeFile( $orig, $new ) { diff --git a/maintenance/importImages.inc.php b/maintenance/importImages.inc.php index c8fbc541de..e7529966e0 100644 --- a/maintenance/importImages.inc.php +++ b/maintenance/importImages.inc.php @@ -47,20 +47,4 @@ function splitFilename( $filename ) { return array( $fname, $ext ); } -/** - * Given an image hash, check that the structure exists to save the image file - * and create it if it doesn't - * - * @param $hash Part of an image hash, e.g. /f/fd/ - */ -function makeHashPath( $hash ) { - global $wgUploadDirectory; - $parts = explode( '/', substr( $hash, 1, strlen( $hash ) - 2 ) ); - if( !is_dir( $wgUploadDirectory . '/' . $parts[0] ) ) - mkdir( $wgUploadDirectory . '/' . $parts[0] ); - if( !is_dir( $wgUploadDirectory . '/' . $hash ) ) - mkdir( $wgUploadDirectory . '/' . $hash ); -} - - -?> \ No newline at end of file +?> diff --git a/maintenance/importImages.php b/maintenance/importImages.php index f2d53989dc..a3a8f83b28 100644 --- a/maintenance/importImages.php +++ b/maintenance/importImages.php @@ -42,56 +42,40 @@ if( count( $args ) > 1 ) { $license = isset( $options['license'] ) ? $options['license'] : ''; # Batch "upload" operation + global $wgUploadDirectory; foreach( $files as $file ) { - $base = wfBaseName( $file ); # Validate a title $title = Title::makeTitleSafe( NS_IMAGE, $base ); - if( is_object( $title ) ) { - - # Check existence - $image = new Image( $title ); - if( !$image->exists() ) { - - global $wgUploadDirectory; - - # copy() doesn't create paths so if the hash path doesn't exist, we - # have to create it - makeHashPath( wfGetHashPath( $image->name ) ); - - # Stash the file - echo( "Saving {$base}..." ); - - if( copy( $file, $image->getFullPath() ) ) { - - echo( "importing..." ); - - # Grab the metadata - $image->loadFromFile(); - - # Record the upload - if( $image->recordUpload( '', $comment, $license ) ) { - - # We're done! - echo( "done.\n" ); + if( !is_object( $title ) ) { + echo( "{$base} could not be imported; a valid title cannot be produced\n" ); + continue; + } - } else { - echo( "failed.\n" ); - } + # Check existence + $image = wfLocalFile( $title ); + if( $image->exists() ) { + echo( "{$base} could not be imported; a file with this name exists in the wiki\n" ); + continue; + } - } else { - echo( "failed.\n" ); - } + # Stash the file + echo( "Saving {$base}..." ); - } else { - echo( "{$base} could not be imported; a file with this name exists in the wiki\n" ); - } + $archive = $image->publish( $file ); + if ( WikiError::isError( $archive ) ) { + echo( "failed.\n" ); + continue; + } + echo( "importing..." ); + if ( $image->recordUpload( $archive, $comment, $license ) ) { + # We're done! + echo( "done.\n" ); } else { - echo( "{$base} could not be imported; a valid title cannot be produced\n" ); + echo( "failed.\n" ); } - } } else { diff --git a/maintenance/rebuildImages.php b/maintenance/rebuildImages.php index 4c02dc9c3b..4aff75b9d7 100644 --- a/maintenance/rebuildImages.php +++ b/maintenance/rebuildImages.php @@ -40,6 +40,16 @@ class ImageBuilder extends FiveUpgrade { $this->maxLag = 10; # if slaves are lagged more than 10 secs, wait $this->dryrun = $dryrun; + if ( $dryrun ) { + $GLOBALS['wgReadOnly'] = 'Dry run mode, image upgrades are suppressed'; + } + } + + function getRepo() { + if ( !isset( $this->repo ) ) { + $this->repo = RepoGroup::singleton()->getLocalRepo(); + } + return $this->repo; } function build() { @@ -94,13 +104,7 @@ class ImageBuilder extends FiveUpgrade { while( $row = $this->dbr->fetchObject( $result ) ) { $update = call_user_func( $callback, $row ); - if( is_array( $update ) ) { - if( !$this->dryrun ) { - $this->dbw->update( $table, - $update, - array( $key => $row->$key ), - $fname ); - } + if( $update ) { $this->progress( 1 ); } else { $this->progress( 0 ); @@ -116,97 +120,43 @@ class ImageBuilder extends FiveUpgrade { } function imageCallback( $row ) { - if( $row->img_width ) { - // Already processed - return null; - } - - // Fill in the new image info fields - $info = $this->imageInfo( $row->img_name ); - - global $wgMemc; - $key = wfMemcKey( "Image", md5( $row->img_name ) ); - $wgMemc->delete( $key ); - - return array( - 'img_width' => $info['width'], - 'img_height' => $info['height'], - 'img_bits' => $info['bits'], - 'img_media_type' => $info['media'], - 'img_major_mime' => $info['major'], - 'img_minor_mime' => $info['minor'] ); + // Create a File object from the row + // This will also upgrade it + $file = $this->getRepo()->newFileFromRow( $row ); + return $file->getUpgraded(); } - function buildOldImage() { $this->buildTable( 'oldimage', 'oi_archive_name', array( &$this, 'oldimageCallback' ) ); } function oldimageCallback( $row ) { - if( $row->oi_width ) { - return null; + // Create a File object from the row + // This will also upgrade it + if ( $row->oi_archive_name == '' ) { + $this->log( "Empty oi_archive_name for oi_name={$row->oi_name}" ); + return false; } - - // Fill in the new image info fields - $info = $this->imageInfo( $row->oi_archive_name, 'wfImageArchiveDir', $row->oi_name ); - return array( - 'oi_width' => $info['width' ], - 'oi_height' => $info['height'], - 'oi_bits' => $info['bits' ] ); + $file = $this->getRepo()->newFileFromRow( $row ); + return $file->getUpgraded(); } function crawlMissing() { - global $wgUploadDirectory, $wgHashedUploadDirectory; - if( $wgHashedUploadDirectory ) { - for( $i = 0; $i < 16; $i++ ) { - for( $j = 0; $j < 16; $j++ ) { - $dir = sprintf( '%s%s%01x%s%02x', - $wgUploadDirectory, - DIRECTORY_SEPARATOR, - $i, - DIRECTORY_SEPARATOR, - $i * 16 + $j ); - $this->crawlDirectory( $dir ); - } - } - } else { - $this->crawlDirectory( $wgUploadDirectory ); - } + $repo = RepoGroup::singleton()->getLocalRepo(); + $repo->enumFilesInFS( array( $this, 'checkMissingImage' ) ); } - function crawlDirectory( $dir ) { - if( !file_exists( $dir ) ) { - return $this->log( "no directory, skipping $dir" ); - } - if( !is_dir( $dir ) ) { - return $this->log( "not a directory?! skipping $dir" ); - } - if( !is_readable( $dir ) ) { - return $this->log( "dir not readable, skipping $dir" ); - } - $source = opendir( $dir ); - if( $source === false ) { - return $this->log( "couldn't open dir, skipping $dir" ); + function checkMissingImage( $fullpath ) { + $fname = 'ImageBuilder::checkMissingImage'; + $filename = wfBaseName( $fullpath ); + if( is_dir( $fullpath ) ) { + return; } - - $this->log( "crawling $dir" ); - while( false !== ( $filename = readdir( $source ) ) ) { - $fullpath = $dir . DIRECTORY_SEPARATOR . $filename; - if( is_dir( $fullpath ) ) { - continue; - } - if( is_link( $fullpath ) ) { - $this->log( "skipping symlink at $fullpath" ); - continue; - } - $this->checkMissingImage( $filename, $fullpath ); + if( is_link( $fullpath ) ) { + $this->log( "skipping symlink at $fullpath" ); + return; } - closedir( $source ); - } - - function checkMissingImage( $filename, $fullpath ) { - $fname = 'ImageBuilder::checkMissingImage'; $row = $this->dbw->selectRow( 'image', array( 'img_name' ), array( 'img_name' => $filename ), @@ -224,7 +174,7 @@ class ImageBuilder extends FiveUpgrade { $fname = 'ImageBuilder::addMissingImage'; $size = filesize( $fullpath ); - $info = $this->imageInfo( $filename ); + $info = $this->imageInfo( $fullpath ); $timestamp = $this->dbw->timestamp( filemtime( $fullpath ) ); global $wgContLang; @@ -242,23 +192,14 @@ class ImageBuilder extends FiveUpgrade { $this->log( "Empty filename for $fullpath" ); return; } - - $fields = array( - 'img_name' => $filename, - 'img_size' => $size, - 'img_width' => $info['width'], - 'img_height' => $info['height'], - 'img_metadata' => '', // filled in on-demand - 'img_bits' => $info['bits'], - 'img_media_type' => $info['media'], - 'img_major_mime' => $info['major'], - 'img_minor_mime' => $info['minor'], - 'img_description' => '(recovered file, missing upload log entry)', - 'img_user' => 0, - 'img_user_text' => 'Conversion script', - 'img_timestamp' => $timestamp ); - if( !$this->dryrun ) { - $this->dbw->insert( 'image', $fields, $fname ); + if ( !$this->dryrun ) { + $file = wfLocalFile( $filename ); + if ( !$file->recordUpload( '', '(recovered file, missing upload log entry)', '', '', '', + false, $timestamp ) ) + { + $this->log( "Error uploading file $fullpath" ); + return; + } } $this->log( $fullpath ); } diff --git a/tests/ArticleTest.php b/tests/ArticleTest.php index 7bda09ed1f..3276fc7a1d 100644 --- a/tests/ArticleTest.php +++ b/tests/ArticleTest.php @@ -1,19 +1,8 @@ PHPUnit_TestCase( $name ); - } - function setUp() { $globalSet = array( 'wgLegacyEncoding' => false, @@ -104,20 +93,6 @@ class ArticleTest extends PHPUnit_TestCase { Revision::getRevisionText( $row ), "getRevisionText" ); } - function testCompressRevisionTextLatin1() { - $GLOBALS['wgUseLatin1'] = true; - $row->old_text = "Wiki est l'\xe9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertFalse( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should not contain 'utf-8'" ); - $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should not contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xe9cole superieur !", - $row->old_text, "Direct check" ); - $this->assertEquals( "Wiki est l'\xe9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } - function testCompressRevisionTextUtf8Gzip() { $GLOBALS['wgCompressRevisions'] = true; $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; @@ -131,23 +106,6 @@ class ArticleTest extends PHPUnit_TestCase { $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", Revision::getRevisionText( $row ), "getRevisionText" ); } - - function testCompressRevisionTextLatin1Gzip() { - $GLOBALS['wgCompressRevisions'] = true; - $GLOBALS['wgUseLatin1'] = true; - $row = new stdClass; - $row->old_text = "Wiki est l'\xe9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertFalse( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should not contain 'utf-8'" ); - $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xe9cole superieur !", - gzinflate( $row->old_text ), "Direct check" ); - $this->assertEquals( "Wiki est l'\xe9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } - } ?> diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php index edb785dd01..c0347a33e2 100644 --- a/tests/DatabaseTest.php +++ b/tests/DatabaseTest.php @@ -1,23 +1,10 @@ PHPUnit_TestCase( $name ); - } - function setUp() { - $this->db = new Database(); - } - - function tearDown() { - unset( $this->db ); + $this->db = wfGetDB( DB_SLAVE ); } function testAddQuotesNull() { diff --git a/tests/GlobalTest.php b/tests/GlobalTest.php index 5de64f8d81..e15556e0da 100644 --- a/tests/GlobalTest.php +++ b/tests/GlobalTest.php @@ -1,32 +1,6 @@ PHPUnit_TestCase( $name ); - } - - function setUp() { - $this->save = array(); - $saveVars = array( 'wgReadOnlyFile' ); - foreach( $saveVars as $var ) { - if( isset( $GLOBALS[$var] ) ) { - $this->save[$var] = $GLOBALS[$var]; - } - } - $GLOBALS['wgReadOnlyFile'] = wfTempDir() . '/testReadOnly-' . mt_rand(); - } - - function tearDown() { - foreach( $this->save as $var => $data ) { - $GLOBALS[$var] = $data; - } - } - +class GlobalTest extends PHPUnit_Framework_TestCase { function testRandom() { # This could hypothetically fail, but it shouldn't ;) $this->assertFalse( diff --git a/tests/ImageTest.php b/tests/ImageFunctionsTest.php similarity index 67% rename from tests/ImageTest.php rename to tests/ImageFunctionsTest.php index 5957bec54a..69d46a8bca 100644 --- a/tests/ImageTest.php +++ b/tests/ImageFunctionsTest.php @@ -1,23 +1,6 @@ PHPUnit_TestCase( $name ); - } - - function setUp() { - } - - function tearDown() { - } - +class ImageFunctionsTest extends PHPUnit_Framework_TestCase { function testFitBoxWidth() { $vals = array( array( @@ -60,8 +43,6 @@ class ImageTest extends PHPUnit_TestCase { } } } - - /* TODO: many more! */ } ?> diff --git a/tests/README b/tests/README index df9571633e..3bbf970407 100644 --- a/tests/README +++ b/tests/README @@ -1,11 +1,9 @@ Some quickie unit tests done with the PHPUnit testing framework. To run the test suite, run 'make test' in this dir or 'php RunTests.php' -You can install PHPUnit via pear like this: -Firstly, register phpunit channel (it only need to be done once): +PHPUnit is no longer maintained by PEAR. To get the current version of +PHPUnit, first uninstall any old version of PHPUnit or PHPUnit2 from PEAR, +then install the current version from phpunit.de like this: + # pear channel-discover pear.phpunit.de -Then install the package: # pear install phpunit/PHPUnit - -Or fetch and install it manually: -http://www.phpunit.de/ diff --git a/tests/RunTests.php b/tests/RunTests.php index 1d088dcb80..aac1eb3e67 100644 --- a/tests/RunTests.php +++ b/tests/RunTests.php @@ -1,25 +1,11 @@ array( @@ -34,10 +20,6 @@ $testOptions = array( 'database' => null ), ); -if( file_exists( 'LocalTestSettings.php' ) ) { - include( './LocalTestSettings.php' ); -} - $tests = array( 'GlobalTest', 'DatabaseTest', @@ -47,14 +29,14 @@ $tests = array( 'ImageTest' ); -if( isset( $_SERVER['argv'][1] ) ) { +if( count( $args ) ) { // to override... - $tests = array( $_SERVER['argv'][1] ); + $tests = $args; } foreach( $tests as $test ) { require_once( $test . '.php' ); - $suite = new PHPUnit_TestSuite( $test ); + $suite = new PHPUnit_Framework_TestSuite( $test ); $result = PHPUnit::run( $suite ); echo $result->toString(); } diff --git a/tests/SanitizerTest.php b/tests/SanitizerTest.php index ea53f910b2..0322da6c05 100644 --- a/tests/SanitizerTest.php +++ b/tests/SanitizerTest.php @@ -1,22 +1,6 @@ PHPUnit_TestCase( $name ); - } - - function setUp() { - } - - function tearDown() { - } - +class SanitizerTest extends PHPUnit_Framework_TestCase { function testDecodeNamed() { $this->assertEquals( "\xc3\xa9cole", diff --git a/tests/SearchEngineTest.php b/tests/SearchEngineTest.php index d7809416f3..1494990b8f 100644 --- a/tests/SearchEngineTest.php +++ b/tests/SearchEngineTest.php @@ -1,20 +1,7 @@ diff --git a/thumb.php b/thumb.php index b92a6f7ef5..e6ad8e5998 100644 --- a/thumb.php +++ b/thumb.php @@ -2,20 +2,15 @@ /** * PHP script to stream out an image thumbnail. - * If the file exists, we make do with abridged MediaWiki initialisation. */ -define( 'MW_NO_SETUP', 1 ); define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); require_once( './includes/WebStart.php' ); wfProfileIn( 'thumb.php' ); wfProfileIn( 'thumb.php-start' ); -require_once( "$IP/includes/GlobalFunctions.php" ); -require_once( "$IP/includes/ImageFunctions.php" ); $wgTrivialMimeDetection = true; //don't use fancy mime detection, just check the file extension for jpg/gif/png. require_once( "$IP/includes/StreamFile.php" ); -require_once( "$IP/includes/AutoLoader.php" ); // Get input parameters if ( get_magic_quotes_gpc() ) { @@ -40,36 +35,30 @@ unset( $params['r'] ); // Some basic input validation $fileName = strtr( $fileName, '\\/', '__' ); -// Work out paths, carefully avoiding constructing an Image object because that won't work yet +// Stream the file if it exists already try { - $handler = thumbGetHandler( $fileName ); - if ( $handler ) { - $imagePath = wfImageDir( $fileName ) . '/' . $fileName; - $thumbName = $handler->makeParamString( $params ) . "-$fileName"; - $thumbPath = wfImageThumbDir( $fileName ) . '/' . $thumbName; + $img = wfLocalFile( $fileName ); + if ( $img && false != ( $thumbName = $img->thumbName( $params ) ) ) { + $thumbPath = $img->getThumbPath( $thumbName ); - if ( is_file( $thumbPath ) && filemtime( $thumbPath ) >= filemtime( $imagePath ) ) { + if ( is_file( $thumbPath ) ) { wfStreamFile( $thumbPath ); - // Can't log profiling data with no Setup.php + wfLogProfilingData(); exit; } } } catch ( MWException $e ) { - require_once( './includes/Setup.php' ); thumbInternalError( $e->getHTML() ); + wfLogProfilingData(); exit; } - -// OK, no valid thumbnail, time to get out the heavy machinery wfProfileOut( 'thumb.php-start' ); -require_once( './includes/Setup.php' ); wfProfileIn( 'thumb.php-render' ); -$img = Image::newFromName( $fileName ); try { if ( $img ) { - $thumb = $img->transform( $params, Image::RENDER_NOW ); + $thumb = $img->transform( $params, File::RENDER_NOW ); } else { $thumb = false; } @@ -80,9 +69,11 @@ try { if ( $thumb && $thumb->getPath() && file_exists( $thumb->getPath() ) ) { wfStreamFile( $thumb->getPath() ); -} elseif ( $img ) { - if ( !$thumb ) { - $msg = wfMsgHtml( 'thumbnail_error', 'Image::transform() returned false' ); +} else { + if ( !$img ) { + $msg = wfMsg( 'badtitletext' ); + } elseif ( !$thumb ) { + $msg = wfMsgHtml( 'thumbnail_error', 'File::transform() returned false' ); } elseif ( $thumb->isError() ) { $msg = $thumb->getHtmlMsg(); } elseif ( !$thumb->getPath() ) { @@ -91,19 +82,6 @@ if ( $thumb && $thumb->getPath() && file_exists( $thumb->getPath() ) ) { $msg = wfMsgHtml( 'thumbnail_error', 'Output file missing' ); } thumbInternalError( $msg ); -} else { - $badtitle = wfMsg( 'badtitle' ); - $badtitletext = wfMsg( 'badtitletext' ); - header( 'Cache-Control: no-cache' ); - header( 'Content-Type: text/html; charset=utf-8' ); - header( 'HTTP/1.1 500 Internal server error' ); - echo " - $badtitle - -

$badtitle

-

$badtitletext

- -"; } wfProfileOut( 'thumb.php-render' ); @@ -112,17 +90,6 @@ wfLogProfilingData(); //-------------------------------------------------------------------------- -function thumbGetHandler( $fileName ) { - // Determine type - $magic = MimeMagic::singleton(); - $extPos = strrpos( $fileName, '.' ); - if ( $extPos === false ) { - return false; - } - $mime = $magic->guessTypesForExtension( substr( $fileName, $extPos + 1 ) ); - return MediaHandler::getHandler( $mime ); -} - function thumbInternalError( $msg ) { header( 'Cache-Control: no-cache' ); header( 'Content-Type: text/html; charset=utf-8' ); -- 2.20.1