From: Ævar Arnfjörð Bjarmason Date: Sat, 21 May 2005 07:46:17 +0000 (+0000) Subject: * (bug 898) Mime type autodetection. X-Git-Tag: 1.5.0alpha2~140 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/recherche.php?a=commitdiff_plain;h=27105c21295dbc16532875feb3582a0555576e73;p=lhc%2Fweb%2Fwiklou.git * (bug 898) Mime type autodetection. --- diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 8ff8b84fac..b16c24131d 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -166,6 +166,8 @@ Various bugfixes, small features, and a few experimental things: * ...various... * (bug 498) The Views heading in MonoBook.php is now localizable +* (bug 898) The wiki can now do advanced sanity check on uploaded files + including virus checks using external programs. * (bug 2067) Fixed crash on empty quoted HTML attribute * (bug 2079) Removed links to Special:Maintenance from movepagetext messages * Fix for reading incorrectly re-gzipped HistoryBlob entries diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index d5ab56c9ec..118ff6bb6f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -110,6 +110,112 @@ $wgTmpDirectory = "{$wgUploadDirectory}/tmp"; $wgUploadBaseUrl = ""; /**#@-*/ +/** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array. + * Set this to NULL to disable virus scanning. If not null, every file uploaded will be scanned for viruses. + * @global string $wgAntivirus + */ +$wgAntivirus= NULL; + +/** Configuration for different virus scanners. This an associative array of associative arrays: + * it contains on setup array per known scanner type. The entry is selected by $wgAntivirus, i.e. + * valid values for $wgAntivirus are the keys defined in this array. + * + * The configuration array for each scanner contains the following keys: "command", "codemap", "messagepattern"; + * + * "command" is the full command to call the virus scanner - %f will be replaced with the name of the + * file to scan. If not present, the filename will be appended to the command. Note that this must be + * overwritten if the scanner is not in the system path; in that case, plase set + * $wgAntivirusSetup[$wgAntivirus]['command'] to the desired command with full path. + * + * "codemap" is a mapping of exit code to return codes of the detectVirus function in SpecialUpload. + * An exit code mapped to AV_SCAN_FAILED causes the function to consider the scan to be failed. This will pass + * the file if $wgAntivirusRequired is not set. + * An exit code mapped to AV_SCAN_ABORTED causes the function to consider the file to have an usupported format, + * which is probably imune to virusses. This causes the file to pass. + * An exit code mapped to AV_NO_VIRUS will cause the file to pass, meaning no virus was found. + * All other codes (like AV_VIRUS_FOUND) will cause the function to report a virus. + * You may use "*" as a key in the array to catch all exit codes not mapped otherwise. + * + * "messagepattern" is a perl regular expression to extract the meaningful part of the scanners + * output. The relevant part should be matched as group one (\1). + * If not defined or the pattern does not match, the full message is shown to the user. + * + * @global array $wgAntivirusSetup + */ +$wgAntivirusSetup= array( + + #setup for clamav + 'clamav' => array ( + 'command' => "clamscan --no-summary ", + + 'codemap'=> array ( + "0"=> AV_NO_VIRUS, #no virus + "1"=> AV_VIRUS_FOUND, #virus found + "52"=> AV_SCAN_ABORTED, #unsupported file format (probably imune) + "*"=> AV_SCAN_FAILED, #else scan failed + ), + + 'messagepattern'=> '/.*?:(.*)/sim', + ), + + #setup for f-prot + 'f-prot' => array ( + 'command' => "f-prot ", + + 'codemap'=> array ( + "0"=> AV_NO_VIRUS, #no virus + "3"=> AV_VIRUS_FOUND, #virus found + "6"=> AV_VIRUS_FOUND, #virus found + "*"=> AV_SCAN_FAILED, #else scan failed + ), + + 'messagepattern'=> '/.*?Infection:(.*)$/m', + ), +); + + +/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. + * @global boolean $wgAntivirusRequired +*/ +$wgAntivirusRequired= true; + +/** Determines if the mime type of uploaded files should be checked + * @global boolean $wgVerifyMimeType +*/ +$wgVerifyMimeType= true; + +/** Sets the mime type definition file to use by MimeMagic.php. +* @global string $wgMimeTypeFile +*/ +#$wgMimeTypeFile= "/etc/mime.types"; +$wgMimeTypeFile= "includes/mime.types"; +#$wgMimeTypeFile= NULL; #use build in defaults only. + +/** Sets the mime type info file to use by MimeMagic.php. +* @global string $wgMimeInfoFile +*/ +$wgMimeInfoFile= "includes/mime.info"; +#$wgMimeInfoFile= NULL; #use build in defaults only. + +/** Switch for loading the FileInfo extension by PECL at runtime. +* This should be used only if fileinfo is installed as a shared object / dynamic libary +* @global string $wgLoadFileinfoExtension +*/ +$wgLoadFileinfoExtension= false; + +/** Sets an external mime detector program. The command must print only the mime type to standard output. +* the name of the file to process will be appended to the command given here. +* If not set or NULL, mime_content_type will be used if available. +* @global string $wgMimeTypeFile +*/ +$wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0 +#$wgMimeDetectorCommand= "file -bi" #use external mime detector (linux) + +/** Switch for trivial mime detection. Used by thumb.php to disable all fance things, +* because only a few types of images are needed and file extensions can be trusted. +*/ +$wgTrivialMimeDetection= false; + /** * Produce hashed HTML article paths. Used internally, do not set. */ @@ -795,13 +901,25 @@ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' ); /** Files with these extensions will never be allowed as uploads. */ $wgFileBlacklist = array( # HTML may contain cookie-stealing JavaScript and web bugs - 'html', 'htm', + 'html', 'htm', 'js', 'jsb', # PHP scripts may execute arbitrary code on the server 'php', 'phtml', 'php3', 'php4', 'phps', # Other types that may be interpreted by some servers 'shtml', 'jhtml', 'pl', 'py', 'cgi', # May contain harmful executables for Windows victims 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl' ); + +/** Files with these mime types will never be allowed as uploads + * if $wgVerifyMimeType is enabled. + */ +$wgMimeTypeBlacklist= array( + # HTML may contain cookie-stealing JavaScript and web bugs + 'text/html', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', + # PHP scripts may execute arbitrary code on the server + 'application/x-php', 'text/x-php', + # Other types that may be interpreted by some servers + 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh' +); /** This is a flag to determine whether or not to check file extensions on upload. */ $wgCheckFileExtensions = true; @@ -1314,4 +1432,24 @@ $wgCountCategorizedImagesAsUsed = false; * CAUTION: Access to database might lead to code execution */ $wgExternalStores = false; + +/** +* list of trusted media-types and mime types. +* Use the MEDIATYPE_xxx constants to represent media types. +* This list is used by Image::isSafeFile +* +* Types not listed here will have a warning about unsafe content +* displayed on the images description page. It would also be possible +* to use this for further restrictions, like disabling direct +* [[media:...]] links for non-trusted formats. +*/ +$wgTrustedMediaFormats= array( + MEDIATYPE_BITMAP, //all bitmap formats + MEDIATYPE_AUDIO, //all audio formats + MEDIATYPE_VIDEO, //all plain video formats + "image/svg", //svg (only needed if inline rendering of svg is not supported) + "application/pdf", //PDF files + #"application/x-shockwafe-flash", //flash/shockwave movie +); + ?> diff --git a/includes/Defines.php b/includes/Defines.php index c49b209b38..6f8b1b9138 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -89,4 +89,34 @@ define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCac define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available /**#@-*/ + + +/**#@+ + * Media types. + * This defines constants for the value returned by Image::getMediaType() + */ + +define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); // unknown format +define( 'MEDIATYPE_BITMAP', 'BITMAP' ); // some bitmap image or image source (like psd, etc). Can't scale up. +define( 'MEDIATYPE_DRAWING', 'DRAWING' ); // some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. +define( 'MEDIATYPE_AUDIO', 'AUDIO' ); // simple audio file (ogg, mp3, wav, midi, whatever) +define( 'MEDIATYPE_VIDEO', 'VIDEO' ); // simple video file (ogg, mpg, etc; no not include formats here that may contain executable sections or scripts!) +define( 'MEDIATYPE_MULTIMEDIA', 'MULTIMEDIA' ); // Scriptable Multimedia (flash, advanced video container formats, etc) +define( 'MEDIATYPE_OFFICE', 'OFFICE' ); // Office Documents, Spreadsheets (office formats possibly containing apples, scripts, etc) +define( 'MEDIATYPE_TEXT', 'TEXT' ); // Plain text (possibly containing program code or scripts) +define( 'MEDIATYPE_EXECUTABLE', 'EXECUTABLE' ); // binary executable +define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); // archive file (zip, tar, etc) +/**#@-*/ + +/**#@+ + * Antivirus result codes, for use in $wgAntivirusSetup. + */ + +define( 'AV_NO_VIRUS', 0 ); #scan ok, no virus found +define( 'AV_VIRUS_FOUND', 1 ); #virus found! +define( 'AV_SCAN_ABORTED', -1 ); #scan aborted, the file is probably imune +define( 'AV_SCAN_FAILED', false ); #scan failed (scanner not found or error in scanner) + +/**#@-*/ + ?> diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index aa3f6d310b..6e011132e2 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -1229,6 +1229,37 @@ function wfElementClean( $element, $attribs = array(), $contents = '') { return wfElement( $element, $attribs, UtfNormal::cleanUp( $contents ) ); } +/** Global singleton instance of MimeMagic. This is initialized on demand, +* please always use the wfGetMimeMagic() function to get the instance. +* +* @private +*/ +$wgMimeMagic= NULL; + +/** Factory functions for the global MimeMagic object. +* This function always returns the same singleton instance of MimeMagic. +* That objects will be instantiated on the first call to this function. +* If needed, the MimeMagic.php file is automatically included by this function. +* @return MimeMagic the global MimeMagic objects. +*/ +function &wfGetMimeMagic() { + global $wgMimeMagic; + + if (!is_null($wgMimeMagic)) { + return $wgMimeMagic; + } + + if (!class_exists("MimeMagic")) { + #include on demand + require_once("MimeMagic.php"); + } + + $wgMimeMagic= new MimeMagic(); + + return $wgMimeMagic; +} + + /** * Tries to get the system directory for temporary files. * The TMPDIR, TMP, and TEMP environment variables are checked in sequence, diff --git a/includes/Image.php b/includes/Image.php index ae28c72162..2b49627e04 100644 --- a/includes/Image.php +++ b/includes/Image.php @@ -37,8 +37,9 @@ class Image $width, # \ $height, # | $bits, # --- returned by getimagesize (loadFromXxx) - $type, # | $attr, # / + $type, # MEDIATYPE_xxx (bitmap, drawing, audio...) + $mime, # MIME type, determined by MimeMagic::guessMimeType $size, # Size in bytes (loadFromXxx) $metadata, # Metadata $exif, # The Exif class @@ -111,7 +112,7 @@ class Image // Check if the key existed and belongs to this version of MediaWiki if (!empty($cachedValues) && is_array($cachedValues) && isset($cachedValues['width']) - && $cachedValues['fileExists'] && isset( $cachedValues['metadata'] ) ) + && $cachedValues['fileExists'] && isset( $cachedValues['mime'] ) && isset( $cachedValues['metadata'] ) ) { if ( $wgUseSharedUploads && $cachedValues['fromShared']) { # if this is shared file, we need to check if image @@ -126,6 +127,7 @@ class Image $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; @@ -142,6 +144,7 @@ class Image $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; @@ -173,6 +176,7 @@ class Image 'height' => $this->height, 'bits' => $this->bits, 'type' => $this->type, + 'mime' => $this->mime, 'metadata' => $this->metadata, 'size' => $this->size); @@ -190,7 +194,9 @@ class Image $this->imagePath = $this->getFullPath(); $this->fileExists = file_exists( $this->imagePath ); $this->fromSharedDirectory = false; - $gis = false; + $gis = array(); + + if (!$this->fileExists) wfDebug("$fname: ".$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) { @@ -206,42 +212,67 @@ class Image } } + if ( $this->fileExists ) { + $magic=& wfGetMimeMagic(); + + $this->mime = $magic->guessMimeType($this->imagePath,true); + $this->type = $magic->getMediaType($this->imagePath,$this->mime); + # Get size in bytes $this->size = filesize( $this->imagePath ); + $magic=& wfGetMimeMagic(); + # Height and width - # Don't try to get the width and height of sound and video files, that's bad for performance - if ( !Image::isKnownImageExtension( $this->extension ) ) { - $gis = false; - } elseif( $this->extension == 'svg' ) { + if( $this->mime == 'image/svg' ) { wfSuppressWarnings(); $gis = wfGetSVGsize( $this->imagePath ); wfRestoreWarnings(); - } else { + } + elseif ( !$magic->isPHPImageType( $this->mime ) ) { + # Don't try to get the width and height of sound and video files, that's bad for performance + $gis[0]= 0; //width + $gis[1]= 0; //height + $gis[2]= 0; //unknown + $gis[3]= ""; //width height string + } + else { wfSuppressWarnings(); $gis = getimagesize( $this->imagePath ); wfRestoreWarnings(); } + + wfDebug("$fname: ".$this->imagePath." loaded, ".$this->size." bytes, ".$this->mime.".\n"); } - if( $gis === false ) { - $this->width = 0; - $this->height = 0; - $this->bits = 0; - $this->type = 0; - $this->metadata = serialize ( array() ) ; - } else { - $this->width = $gis[0]; - $this->height = $gis[1]; - $this->type = $gis[2]; - $this->metadata = serialize ( $this->retrieveExifData() ) ; - if ( isset( $gis['bits'] ) ) { - $this->bits = $gis['bits']; - } else { - $this->bits = 0; - } + else { + $gis[0]= 0; //width + $gis[1]= 0; //height + $gis[2]= 0; //unknown + $gis[3]= ""; //width height string + + $this->mime = NULL; + $this->type = MEDIATYPE_UNKNOWN; + wfDebug("$fname: ".$this->imagePath." NOT FOUND!\n"); } + + $this->width = $gis[0]; + $this->height = $gis[1]; + + #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 ($this->fileExists) $this->metadata = serialize ( $this->retrieveExifData() ) ; + else $this->metadata = serialize ( array() ) ; + + if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; + else $this->bits = 0; + wfProfileOut( $fname ); } @@ -254,8 +285,12 @@ class Image wfProfileIn( $fname ); $dbr =& wfGetDB( DB_SLAVE ); + + $this->checkDBSchema($dbr); + $row = $dbr->selectRow( 'image', - array( 'img_size', 'img_width', 'img_height', 'img_bits', 'img_type' , 'img_metadata' ), + 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 ), $fname ); if ( $row ) { $this->fromSharedDirectory = false; @@ -263,7 +298,7 @@ class Image $this->loadFromRow( $row ); $this->imagePath = $this->getFullPath(); // Check for rows from a previous schema, quietly upgrade them - if ( $this->type == -1 ) { + if ( is_null($this->type) ) { $this->upgradeRow(); } } elseif ( $wgUseSharedUploads && $wgSharedUploadDBname ) { @@ -273,7 +308,7 @@ class Image $name = $wgLang->ucfirst($this->name); $row = $dbr->selectRow( "`$wgSharedUploadDBname`.image", - array( 'img_size', 'img_width', 'img_height', 'img_bits', 'img_type' ), + array( 'img_size', 'img_width', 'img_height', 'img_bits', 'img_media_type', 'img_major_mime', 'img_minor_mime' ), array( 'img_name' => $name ), $fname ); if ( $row ) { $this->fromSharedDirectory = true; @@ -283,7 +318,7 @@ class Image $this->loadFromRow( $row ); // Check for rows from a previous schema, quietly upgrade them - if ( $this->type == -1 ) { + if ( is_null($this->type) ) { $this->upgradeRow(); } } @@ -312,9 +347,20 @@ class Image $this->width = $row->img_width; $this->height = $row->img_height; $this->bits = $row->img_bits; - $this->type = $row->img_type; + $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; if ( $this->metadata == "" ) $this->metadata = serialize ( array() ) ; + $this->dataLoaded = true; } @@ -355,12 +401,27 @@ class Image // This avoids breaking replication in MySQL $dbw->selectDB( $wgSharedUploadDBname ); } + + $this->checkDBSchema($dbw); + + if (strpos($this->mime,'/')!==false) { + list($major,$minor)= explode('/',$this->mime,2); + } + else { + $major= $this->mime; + $minor= "unknown"; + } + + wfDebug("$fname: upgrading ".$this->name." to 1.5 schema\n"); + $dbw->update( 'image', array( 'img_width' => $this->width, 'img_height' => $this->height, 'img_bits' => $this->bits, - 'img_type' => $this->type, + 'img_media_type' => $this->type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, 'img_metadata' => $this->metadata, ), array( 'img_name' => $this->name ), $fname ); @@ -402,8 +463,14 @@ class Image } function getViewURL() { - if( $this->mustRender() ) { - return $this->createThumb( $this->getWidth() ); + 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(); } @@ -451,19 +518,157 @@ class Image } /** - * Return the type of the image - * - * - 1 GIF - * - 2 JPG - * - 3 PNG - * - 15 WBMP - * - 16 XBM + * 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 getType() { + 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. + */ + function canRender() { + global $wgUseImageMagick; + + if( $this->getWidth()<=0 || $this->getHeight()<=0 ) return false; + + $mime= $this->getMimeType(); + + if (!$mime || $mime==='unknown' || $mime==='unknown/unknown') return false; + + #if it's SVG, check if ther's a converter enabled + if ($mime === 'image/svg') { + global $wgSVGConverters, $wgSVGConverter; + + if ($wgSVGConverter && isset( $wgSVGConverters[$wgSVGConverter])) { + return true; + } + } + + #image formats available on ALL browsers + if ( $mime === 'image/gif' + || $mime === 'image/png' + || $mime === 'image/jpeg' ) return true; + + #image formats that can be converted to the above formats + if ($wgUseImageMagick) { + #convertable by ImageMagick (there are more...) + if ( $mime === 'image/vnd.wap.wbmp' + || $mime === 'image/x-xbitmap' + || $mime === 'image/x-xpixmap' + #|| $mime === 'image/x-icon' #file may be split into multiple parts + || $mime === 'image/x-portable-anymap' + || $mime === 'image/x-portable-bitmap' + || $mime === 'image/x-portable-graymap' + || $mime === 'image/x-portable-pixmap' + #|| $mime === 'image/x-photoshop' #this takes a lot of CPU and RAM! + || $mime === 'image/x-rgb' + || $mime === 'image/x-bmp' + || $mime === 'image/tiff' ) return true; + } + else { + #convertable by the PHP GD image lib + if ( $mime === 'image/vnd.wap.wbmp' + || $mime === 'image/x-xbitmap' ) return true; + } + + return false; + } + + + /** + * 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() { + $mime= $this->getMimeType(); + + if ( $mime === "image/gif" + || $mime === "image/png" + || $mime === "image/jpeg" ) return false; + + return true; + } + + /** + * 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 scurity risc. + */ + 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 ture if allowInlineDisplay() + * or isTrustedFile() is true for this file. + */ + 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 flagegd 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 + * coupeled with cryptographics signatures or such. + */ + function isTrustedFile() { + #this could be implemented to check a flag in the databas, + #look for signatures, etc + return false; + } + /** * Return the escapeLocalURL of this image * @access public @@ -537,6 +742,9 @@ class Image } if ( $script ) { $url = $script . '?f=' . urlencode( $this->name ) . '&w=' . urlencode( $width ); + if( $this->mustRender() ) { + $url.= '&r=1'; + } } else { $name = $this->thumbName( $width ); if($this->fromSharedDirectory) { @@ -567,9 +775,17 @@ class Image */ function thumbName( $width ) { $thumb = $width."px-".$this->name; - if( $this->extension == 'svg' ) { - # Rasterize SVG vector images to PNG - $thumb .= '.png'; + + if( $this->mustRender() ) { + if( $this->canRender() ) { + # Rasterize to PNG (for SVG vector images, etc) + $thumb .= '.png'; + } + else { + #should we use iconThumb here to get a symbolic thumbnail? + #or should we fail with an internal error? + return NULL; //can't make bitmap + } } return $thumb; } @@ -611,18 +827,24 @@ class Image return $this->renderThumb( $width ); } $this->load(); - if ( $width < $this->width ) { - $thumbheight = $this->height * $width / $this->width; - $thumbwidth = $width; - } else { - $thumbheight = $this->height; - $thumbwidth = $this->width; - } - if ( $thumbheight > $height ) { - $thumbwidth = $thumbwidth * $height / $thumbheight; - $thumbheight = $height; + + if ($this->canRender()) { + if ( $width < $this->width ) { + $thumbheight = $this->height * $width / $this->width; + $thumbwidth = $width; + } else { + $thumbheight = $this->height; + $thumbwidth = $this->width; + } + if ( $thumbheight > $height ) { + $thumbwidth = $thumbwidth * $height / $thumbheight; + $thumbheight = $height; + } + + $thumb = $this->renderThumb( $thumbwidth ); } - $thumb = $this->renderThumb( $thumbwidth ); + else $thumb= NULL; #not a bitmap or renderable image, don't try. + if( is_null( $thumb ) ) { $thumb = $this->iconThumb(); } @@ -672,7 +894,7 @@ class Image } # Sanity check $width - if( $width <= 0 ) { + if( $width <= 0 || $this->width <= 0) { # BZZZT return null; } @@ -721,11 +943,13 @@ class Image } } } + return new ThumbnailImage( $url, $width, $height, $thumbPath ); } // END OF function renderThumb /** * Really render a thumbnail + * Call this only for images for which canRender() returns true. * * @access private */ @@ -735,7 +959,9 @@ class Image $this->load(); - if( $this->extension == 'svg' ) { + if( $this->mime === "image/svg" ) { + #Right now we have only SVG + global $wgSVGConverters, $wgSVGConverter; if( isset( $wgSVGConverters[$wgSVGConverter] ) ) { global $wgSVGConverterPath; @@ -743,8 +969,8 @@ class Image array( '$path/', '$width', '$input', '$output' ), array( $wgSVGConverterPath, $width, - escapeshellarg( $this->imagePath ), - escapeshellarg( $thumbPath ) ), + wfEscapeShellArg( $this->imagePath ), + wfEscapeShellArg( $thumbPath ) ), $wgSVGConverters[$wgSVGConverter] ); $conv = shell_exec( $cmd ); } else { @@ -756,8 +982,9 @@ class Image # in Internet Explorer/Windows instead of default black. $cmd = $wgImageMagickConvertCommand . " -quality 85 -background white -geometry {$width} ". - escapeshellarg($this->imagePath) . " " . - escapeshellarg($thumbPath); + wfEscapeShellArg($this->imagePath) . " " . + wfEscapeShellArg($thumbPath); + wfDebug("reallyRenderThumb: running ImageMagick: $cmd"); $conv = shell_exec( $cmd ); } else { # Use PHP's builtin GD library functions. @@ -814,6 +1041,7 @@ class Image imagedestroy( $dst_image ); imagedestroy( $src_image ); } + # # Check for zero-sized thumbnails. Those can be generated when # no disk space is available or some other error occurs @@ -884,6 +1112,21 @@ class Image wfPurgeSquidServers( $urls ); } } + + function checkDBSchema(&$db) { + # img_name must be unique + if ( !$db->indexUnique( 'image', 'img_name' ) && !$db->indexExists('image','PRIMARY') ) { + wfDebugDieBacktrace( 'Database schema not up to date, please run maintenance/archives/patch-image_name_unique.sql' ); + } + + #new fields must exist + if ( !$db->fieldExists( 'image', 'img_media_type' ) + || !$db->fieldExists( 'image', 'img_metadata' ) + || !$db->fieldExists( 'image', 'img_width' ) ) { + + wfDebugDieBacktrace( 'Database schema not up to date, please run maintenance/updater.php' ); + } + } /** * Return the image history of this image, line by line. @@ -898,6 +1141,9 @@ class Image function nextHistoryLine() { $fname = 'Image::nextHistoryLine()'; $dbr =& wfGetDB( DB_SLAVE ); + + $this->checkDBSchema($dbr); + 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', "'' AS oi_archive_name" ), @@ -926,16 +1172,6 @@ class Image function resetHistory() { $this->historyLine = 0; } - - /** - * Return true if the file is of a type that can't be directly - * rendered by typical browsers and needs to be re-rasterized. - * @return bool - */ - function mustRender() { - $this->load(); - return ( $this->extension == 'svg' ); - } /** * Return the full filesystem path to the file. Note that this does @@ -977,15 +1213,6 @@ class Image return $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory; } - /** - * @return bool - * @static - */ - function isKnownImageExtension( $ext ) { - static $extensions = array( 'svg', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'xbm' ); - return in_array( $ext, $extensions ); - } - /** * Record an image upload in the upload log and the image table */ @@ -996,16 +1223,14 @@ class Image $fname = 'Image::recordUpload'; $dbw =& wfGetDB( DB_MASTER ); - # img_name must be unique - if ( !$dbw->indexUnique( 'image', 'img_name' ) && !$dbw->indexExists('image','PRIMARY') ) { - wfDebugDieBacktrace( 'Database schema not up to date, please run maintenance/archives/patch-image_name_unique.sql' ); - } + $this->checkDBSchema($dbw); // 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; } @@ -1019,6 +1244,15 @@ class Image $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. @@ -1029,7 +1263,9 @@ class Image 'img_width' => $this->width, 'img_height' => $this->height, 'img_bits' => $this->bits, - 'img_type' => $this->type, + 'img_media_type' => $this->type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, 'img_timestamp' => $now, 'img_description' => $desc, 'img_user' => $wgUser->getID(), @@ -1059,14 +1295,13 @@ class Image 'oi_width' => 'img_width', 'oi_height' => 'img_height', 'oi_bits' => 'img_bits', - 'oi_type' => 'img_type', 'oi_timestamp' => 'img_timestamp', 'oi_description' => 'img_description', 'oi_user' => 'img_user', 'oi_user_text' => 'img_user_text', ), array( 'img_name' => $this->name ), $fname ); - + # Update the current image row $dbw->update( 'image', array( /* SET */ @@ -1074,11 +1309,13 @@ class Image 'img_width' => $this->width, 'img_height' => $this->height, 'img_bits' => $this->bits, - 'img_type' => $this->type, + '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_description' => $desc, 'img_metadata' => $this->metadata, ), array( /* WHERE */ 'img_name' => $this->name @@ -1148,7 +1385,7 @@ class Image * @return array */ function retrieveExifData () { - if ( $this->type !== '2' ) return array (); + if ( $this->getMimeType() !== "image/jpeg" ) return array (); $exif = exif_read_data( $this->imagePath ); foreach($exif as $k => $v) { @@ -1208,6 +1445,9 @@ class Image # Update EXIF data in database $dbw =& wfGetDB( DB_MASTER ); + + $this->checkDBSchema($dbw); + $dbw->update( 'image', array( 'img_metadata' => $this->metadata ), array( 'img_name' => $this->name ), @@ -1450,7 +1690,7 @@ function wfIsBadImage( $name ) { return array_key_exists( $name, $titleList ); } - + /** diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 66f6628630..6e8276ec62 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -136,10 +136,14 @@ class ImagePage extends Article { $sk = $wgUser->getSkin(); if ( $this->img->exists() ) { - if ( $this->img->getType() ) { + # image + $width = $this->img->getWidth(); + $height = $this->img->getHeight(); + $showLink = false; + + if ( $this->img->allowInlineDisplay() and $width and $height) { # image - $width = $this->img->getWidth(); - $height = $this->img->getHeight(); + # "Download high res version" link below the image $msg = wfMsg('showbigimage', $width, $height, intval( $this->img->getSize()/1024 ) ); if ( $width > $maxWidth ) { @@ -150,7 +154,8 @@ class ImagePage extends Article { $width = floor( $width * $maxHeight / $height ); $height = $maxHeight; } - if ( $width != $this->img->getWidth() || $height != $this->img->getHeight() ) { + if ( !$this->img->mustRender() + && ( $width != $this->img->getWidth() || $height != $this->img->getHeight() ) ) { if( $wgUseImageResize ) { $thumbnail = $this->img->getThumbnail( $width ); $url = $thumbnail->getUrl(); @@ -163,14 +168,57 @@ class ImagePage extends Article { $anchorclose = "
\n$anchoropen{$msg}"; } else { $url = $full_url; + $showLink = $this->img->mustRender(); } - $s = '' ); } else { - $s = "
" . $sk->makeMediaLink( $this->img->getName(),'' ) . '
'; + #if direct link is allowed but it's not a renderable image, show an icon. + if ($this->img->isSafeFile()) { + $icon= $this->img->iconThumb(); + + $wgOut->addHTML( '' ); + } + + $showLink = true; } - $wgOut->addHTML( $s ); + + + if ($showLink) { + $s= $sk->makeMediaLink( $this->img->getName(), '', '', true ); + $info= wfMsg( 'fileinfo', ceil($this->img->getSize()/1024.0), $this->img->getMimeType() ); + + if (!$this->img->isSafeFile()) { + $wgOut->addHTML("
"); + $wgOut->addHTML(""); + $wgOut->addHTML($s); + $wgOut->addHTML(""); + + $wgOut->addHTML(" ("); + $wgOut->addWikiText( $info, false ); + $wgOut->addHTML(")"); + $wgOut->addHTML("
"); + + #this should be formated a little nicer. Is CSS sufficient? + $wgOut->addHTML("
"); + $wgOut->addWikiText( wfMsg( 'mediawarning' ) ); + $wgOut->addHTML('
'); + + } else { + $wgOut->addHTML("
"); + $wgOut->addHTML($s); + + $wgOut->addHTML(" ("); + $wgOut->addWikiText( $info, false ); + $wgOut->addHTML(")"); + + $wgOut->addHTML("
"); + } + } + if($this->img->fromSharedDirectory) { $sharedtext="
" . wfMsg("sharedupload"); if($wgRepositoryBaseUrl) { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php new file mode 100644 index 0000000000..076dcc215c --- /dev/null +++ b/includes/MimeMagic.php @@ -0,0 +1,657 @@ +mMimeToExt= array(); + $this->mToMime= array(); + + $lines= explode("\n",$types); + foreach ($lines as $s) { + $s= trim($s); + if (empty($s)) continue; + if (strpos($s,'#')===0) continue; + + $s= strtolower($s); + $i= strpos($s,' '); + + if ($i===false) continue; + + #print "processing MIME line $s
"; + + $mime= substr($s,0,$i); + $ext= trim(substr($s,$i+1)); + + if (empty($ext)) continue; + + if (@$this->mMimeToExt[$mime]) $this->mMimeToExt[$mime] .= ' '.$ext; + else $this->mMimeToExt[$mime]= $ext; + + $extensions= explode(' ',$ext); + + foreach ($extensions as $e) { + $e= trim($e); + if (empty($e)) continue; + + if (@$this->mExtToMime[$e]) $this->mExtToMime[$e] .= ' '.$mime; + else $this->mExtToMime[$e]= $mime; + } + } + + /* + * --- load mime.info --- + */ + + global $wgMimeInfoFile; + + $info= MM_WELL_KNOWN_MIME_INFO; + + if ($wgMimeInfoFile) { + if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) { + wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n"); + + $info.= "\n"; + $info.= file_get_contents($wgMimeInfoFile); + } + else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n"); + } + else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n"); + + $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info); + $info= str_replace("\t"," ",$info); + + $this->mMimeTypeAliases= array(); + $this->mMediaTypes= array(); + + $lines= explode("\n",$info); + foreach ($lines as $s) { + $s= trim($s); + if (empty($s)) continue; + if (strpos($s,'#')===0) continue; + + $s= strtolower($s); + $i= strpos($s,' '); + + if ($i===false) continue; + + #print "processing MIME INFO line $s
"; + + $match= array(); + if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) { + $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s); + $mtype= trim(strtoupper($match[1])); + } + else $mtype= MEDIATYPE_UNKNOWN; + + $m= explode(' ',$s); + + if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array(); + + foreach ($m as $mime) { + $mime= trim($mime); + if (empty($mime)) continue; + + $this->mMediaTypes[$mtype][]= $mime; + } + + if (sizeof($m)>1) { + $main= $m[0]; + for ($i=1; $imMimeTypeAliases[$mime]= $main; + } + } + } + + } + + /** returns a list of file extensions for a given mime type + * as a space separated string. + */ + function getExtensionsForType($mime) { + $mime= strtolower($mime); + + $r= @$this->mMimeToExt[$mime]; + + if (@!$r and isset($this->mMimeTypeAliases[$mime])) { + $mime= $this->mMimeTypeAliases[$mime]; + $r= @$this->mMimeToExt[$mime]; + } + + return $r; + } + + /** returns a list of mime types for a given file extension + * as a space separated string. + */ + function getTypesForExtension($ext) { + $ext= strtolower($ext); + + $r= @$this->mExtToMime[$ext]; + return $r; + } + + /** returns a single mime type for a given file extension. + * This is always the first type from the list returned by getTypesForExtension($ext). + */ + function guessTypesForExtension($ext) { + $m= $this->getTypesForExtension( $ext ); + if( is_null($m) ) return NULL; + + $m= trim( $m ); + $m= preg_replace('/\s.*$/','',$m); + + return $m; + } + + + /** tests if the extension matches the given mime type. + * returns true if a match was found, NULL if the mime type is unknown, + * and false if the mime type is known but no matches where found. + */ + function isMatchingExtension($extension,$mime) { + $ext= $this->getExtensionsForType($mime); + + if (!$ext) { + return NULL; //unknown + } + + $ext= explode(' ',$ext); + + $extension= strtolower($extension); + if (in_array($extension,$ext)) { + return true; + } + + return false; + } + + /** returns true if the mime type is known to represent + * an image format supported by the PHP GD library. + */ + function isPHPImageType( $mime ) { + #as defined by imagegetsize and image_type_to_mime + static $types = array( + 'image/gif', 'image/jpeg', 'image/png', + 'image/x-bmp', 'image/xbm', 'image/tiff', + 'image/jp2', 'image/jpeg2000', 'image/iff', + 'image/xbm', 'image/x-xbitmap', + 'image/vnd.wap.wbmp', 'image/vnd.xiff', + 'image/x-photoshop', + 'application/x-shockwave-flash', + ); + + return in_array( $mime, $types ); + } + + + /** mime type detection. This uses detectMimeType to detect the mim type of the file, + * but applies additional checks to determine some well known file formats that may be missed + * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG). + * + * @param string $file The file to check + * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * + * @return string the mime type of $file + */ + function guessMimeType( $file, $useExt=true ) { + $fname = 'MimeMagic::guessMimeType'; + $mime= $this->detectMimeType($file,$useExt); + + if (strpos($mime,"text/")===0 || + $mime==="application/xml") { + + // Read a chunk of the file + $f = fopen( $file, "rt" ); + if( !$f ) return "unknown/unknown"; + $head = fread( $f, 1024 ); + fclose( $f ); + + $xml_type= NULL; + $script_type= NULL; + + /* + * look for XML formats (XHTML and SVG) + */ + if ($mime==="text/sgml" || + $mime==="text/plain" || + $mime==="text/html" || + $mime==="text/xml" || + $mime==="application/xml") { + + if (substr($head,0,5)=="%sim',$head,$match)) $doctype= $match[1]; + if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1]; + + #print "
ANALYSING $file ($mime): doctype= $doctype; tag= $tag
"; + + if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg"; + elseif ($tag==="svg") $mime= "image/svg"; + elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html"; + elseif ($tag==="html") $mime= "text/html"; + + $test_more= false; + } + } + + /* + * look for shell scripts + */ + if (!$xml_type) { + $script_type= NULL; + + #detect by shebang + if (substr($head,0,2)=="#!") $script_type= "ASCII"; + elseif (substr($head,0,5)=="\xef\xbb\xbf#!") $script_type= "UTF-8"; + elseif (substr($head,0,7)=="\xfe\xff\x00#\x00!") $script_type= "UTF-16BE"; + elseif (substr($head,0,7)=="\xff\xfe#\x00!") $script_type= "UTF-16LE"; + + if ($script_type) { + if ($script_type!=="UTF-8" && $script_type!=="ASCII") $head= iconv($script_type,"ASCII//IGNORE",$head); + + $match= array(); + $prog= ""; + + if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) $script= $match[2]; + + $mime= "application/x-$prog"; + } + } + + /* + * look for PHP + */ + if( !$xml_type && !$script_type ) { + + if( ( strpos( $head, 'mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime]; + + wfDebug("$fname: final mime type of $file: $mime\n"); + return $mime; + } + + /** Internal mime type detection, please use guessMimeType() for application code instead. + * Detection is done using an external program, if $wgMimeDetectorCommand is set. + * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available. + * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension. + * If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image. + * If no mime type can be determined, this function returns "unknown/unknown". + * + * @param string $file The file to check + * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * + * @return string the mime type of $file + * @private + */ + function detectMimeType( $file, $useExt=true ) { + $fname = 'MimeMagic::detectMimeType'; + + global $wgMimeDetectorCommand; + + $m= NULL; + if ($wgMimeDetectorCommand) { + $fn= wfEscapeShellArg($file); + $m= `$wgMimeDetectorCommand $fn`; + } + else if (function_exists("finfo_open") && function_exists("finfo_file")) { + + # This required the fileinfo extension by PECL, + # see http://pecl.php.net/package/fileinfo + # This must be compiled into PHP + # + # finfo is the official replacement for the deprecated + # mime_content_type function, see below. + # + # If you may need to load the fileinfo extension at runtime, set + # $wgLoadFileinfoExtension in LocalSettings.php + + $mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */ + + if ($mime_magic_resource) { + $m= finfo_file($mime_magic_resource, $file); + + finfo_close($mime_magic_resource); + } + else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n"); + } + else if (function_exists("mime_content_type")) { + + # NOTE: this function is available since PHP 4.3.0, but only if + # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic. + # + # On Winodws, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; + # sometimes, this may even be needed under linus/unix. + # + # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above. + # see http://www.php.net/manual/en/ref.mime-magic.php for details. + + $m= mime_content_type($file); + } + else wfDebug("$fname: no magic mime detector found!\n"); + + if ($m) { + #normalize + $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc + $m= trim($m); + $m= strtolower($m); + + if (strpos($m,'unknown')!==false) $m= NULL; + else { + wfDebug("$fname: magic mime type of $file: $m\n"); + return $m; + } + } + + #if still not known, use getimagesize to find out the type of image + #TODO: skip things that do not have a well-known image extension? Would that be safe? + wfSuppressWarnings(); + $gis = getimagesize( $file ); + wfRestoreWarnings(); + + $notAnImage= false; + + if ($gis && is_array($gis) && $gis[2]) { + switch ($gis[2]) { + case IMAGETYPE_GIF: $m= "image/gif"; break; + case IMAGETYPE_JPEG: $m= "image/jpeg"; break; + case IMAGETYPE_PNG: $m= "image/png"; break; + case IMAGETYPE_SWF: $m= "application/x-shockwave-flash"; break; + case IMAGETYPE_PSD: $m= "application/photoshop"; break; + case IMAGETYPE_BMP: $m= "image/bmp"; break; + case IMAGETYPE_TIFF_II: $m= "image/tiff"; break; + case IMAGETYPE_TIFF_MM: $m= "image/tiff"; break; + case IMAGETYPE_JPC: $m= "image"; break; + case IMAGETYPE_JP2: $m= "image/jpeg2000"; break; + case IMAGETYPE_JPX: $m= "image/jpeg2000"; break; + case IMAGETYPE_JB2: $m= "image"; break; + case IMAGETYPE_SWC: $m= "application/x-shockwave-flash"; break; + case IMAGETYPE_IFF: $m= "image/vnd.xiff"; break; + case IMAGETYPE_WBMP: $m= "image/vnd.wap.wbmp"; break; + case IMAGETYPE_XBM: $m= "image/x-xbitmap"; break; + } + + if ($m) { + wfDebug("$fname: image mime type of $file: $m\n"); + return $m; + } + else $notAnImage= true; + } + + #if desired, look at extension as a fallback. + if ($useExt) { + $i = strrpos( $file, '.' ); + $e= strtolower( $i ? substr( $file, $i + 1 ) : '' ); + + $m= $this->guessTypesForExtension($e); + + #TODO: if $notAnImage is set, do not trust the file extension if + # the results is one of the image types that should have been recognized + # by getimagesize + + if ($m) { + wfDebug("$fname: extension mime type of $file: $m\n"); + return $m; + } + } + + #unknown type + wfDebug("$fname: failed to guess mime type for $file!\n"); + return "unknown/unknown"; + } + + /** + * Determine the media type code for a file, using its mime type, name and possibly + * its contents. + * + * This function relies on the findMediaType(), mapping extensions and mime + * types to media types. + * + * @todo analyse file if need be + * @todo look at multiple extension, separately and together. + * + * @param string $path full path to the image file, in case we have to look at the contents + * (if null, only the mime type is used to determine the media type code). + * @param string $mime mime type. If null it will be guessed using guessMimeType. + * + * @return (int?string?) a value to be used with the MEDIATYPE_xxx constants. + */ + function getMediaType($path=NULL,$mime=NULL) { + if( !$mime && !$path ) return MEDIATYPE_UNKNOWN; + + #if mime type is unknown, guess it + if( !$mime ) $mime= $this->guessMimeType($path,false); + + #special code for ogg - detect if it's video (theora), + #else label it as sound. + if( $mime=="application/ogg" && file_exists($path) ) { + + // Read a chunk of the file + $f = fopen( $path, "rt" ); + if( !$f ) return MEDIATYPE_UNKNOWN; + $head = fread( $f, 256 ); + fclose( $f ); + + $head= strtolower( $head ); + + #This is an UGLY HACK, file should be parsed correctly + if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO; + elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO; + elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO; + elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO; + else return MEDIATYPE_MULTIMEDIA; + } + + #check for entry for full mime type + if( $mime ) { + $type= $this->findMediaType($mime); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + + #check for entry for file extension + $e= NULL; + if( $path ) { + $i = strrpos( $path, '.' ); + $e= strtolower( $i ? substr( $path, $i + 1 ) : '' ); + + #TODO: look at multi-extension if this fails, parse from full path + + $type= $this->findMediaType('.'.$e); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + + #check major mime type + if( $mime ) { + $i= strpos($mime,'/'); + if( $i !== false ) { + $major= substr($mime,0,$i); + $type= $this->findMediaType($major); + if( $type!==MEDIATYPE_UNKNOWN ) return $type; + } + } + + if( !$type ) $type= MEDIATYPE_UNKNOWN; + + return $type; + } + + /** returns a media code matching the given mime type or file extension. + * File extensions are represented by a string starting with a dot (.) to + * distinguish them from mime types. + * + * This funktion relies on the mapping defined by $this->mMediaTypes + * @private + */ + function findMediaType($extMime) { + + if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types + $m= $this->getTypesForExtension(substr($extMime,1)); + if (!$m) return MEDIATYPE_UNKNOWN; + + $m= explode(' ',$m); + } + else { #normalize mime type + if (isset($this->mMimeTypeAliases[$extMime])) { + $extMime= $this->mMimeTypeAliases[$extMime]; + } + + $m= array($extMime); + } + + foreach ($m as $mime) { + foreach ($this->mMediaTypes as $type => $codes) { + if (in_array($mime,$codes,true)) return $type; + } + } + + return MEDIATYPE_UNKNOWN; + } +} + +?> diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index 74fc78bd19..dd3181af41 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -207,7 +207,7 @@ class UploadForm { return $this->uploadError( wfMsg( 'protectedpage' ) ); } - /* Don't allow users to override the blacklist */ + /* Don't allow users to override the blacklist (check file extension) */ global $wgStrictFileExtensions; global $wgFileExtensions, $wgFileBlacklist; if( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || @@ -221,8 +221,12 @@ class UploadForm { * type but it's corrupt or data of the wrong type, we should * probably not accept it. */ - if( !$this->mStashed && !$this->verify( $this->mUploadTempName, $finalExt ) ) { - return $this->uploadError( wfMsg( 'uploadcorrupt' ) ); + if( !$this->mStashed ) { + $veri= $this->verify($this->mUploadTempName, $finalExt); + + if( $veri !== true ) { //it's a wiki error... + return $this->uploadError( $veri->toString() ); + } } /** @@ -309,6 +313,8 @@ class UploadForm { function saveUploadedFile( $saveName, $tempName, $useRename = false ) { global $wgUploadDirectory, $wgOut; + $fname= "SpecialUpload::saveUploadedFile"; + $dest = wfImageDir( $saveName ); $archive = wfImageArchiveDir( $saveName ); $this->mSavedFile = "{$dest}/{$saveName}"; @@ -324,7 +330,9 @@ class UploadForm { "${archive}/{$this->mUploadOldVersion}" ); return false; } - } else { + else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n"); + } + else { $this->mUploadOldVersion = ''; } @@ -336,6 +344,8 @@ class UploadForm { if( ! $success ) { $wgOut->fileCopyError( $tempName, $this->mSavedFile ); return false; + } else { + wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n"); } } else { wfSuppressWarnings(); @@ -346,7 +356,9 @@ class UploadForm { $wgOut->fileCopyError( $tempName, $this->mSavedFile ); return false; } + else wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n"); } + chmod( $this->mSavedFile, 0644 ); return true; } @@ -640,86 +652,47 @@ class UploadForm { } /** - * Returns false if the file is of a known type but can't be recognized, - * indicating a corrupt file. - * Returns true otherwise; unknown file types are not checked if given - * with an unrecognized extension. + * Verifies that it's ok to include the uploaded file * - * @param string $tmpfile Pathname to the temporary upload file + * @param string $tmpfile the full path opf the temporary file to verify * @param string $extension The filename extension that the file is to be served with - * @return bool + * @return mixed true of the file is verified, a WikiError object otherwise. */ function verify( $tmpfile, $extension ) { - if( $this->triggersIEbug( $tmpfile ) || - $this->triggersSafariBug( $tmpfile ) ) { - return false; - } + #magically determine mime type + $magic=& wfGetMimeMagic(); + $mime= $magic->guessMimeType($tmpfile,false); - $fname = 'SpecialUpload::verify'; - $mergeExtensions = array( - 'jpg' => 'jpeg', - 'tif' => 'tiff' ); - $extensionTypes = array( - # See http://www.php.net/getimagesize - 1 => 'gif', - 2 => 'jpeg', - 3 => 'png', - 4 => 'swf', - 5 => 'psd', - 6 => 'bmp', - 7 => 'tiff', - 8 => 'tiff', - 9 => 'jpc', - 10 => 'jp2', - 11 => 'jpx', - 12 => 'jb2', - 13 => 'swc', - 14 => 'iff', - 15 => 'wbmp', - 16 => 'xbm' ); - - $extension = strtolower( $extension ); - if( isset( $mergeExtensions[$extension] ) ) { - $extension = $mergeExtensions[$extension]; - } - wfDebug( "$fname: Testing file '$tmpfile' with given extension '$extension'\n" ); - - if( !in_array( $extension, $extensionTypes ) ) { - # Not a recognized image type. We don't know how to verify these. - # They're allowed by policy or they wouldn't get this far, so we'll - # let them slide for now. - wfDebug( "$fname: Unknown extension; passing.\n" ); - return true; - } + $fname= "SpecialUpload::verify"; - wfSuppressWarnings(); - $data = getimagesize( $tmpfile ); - wfRestoreWarnings(); - if( false === $data ) { - # Didn't recognize the image type. - # Either the image is corrupt or someone's slipping us some - # bogus data such as HTML+JavaScript trying to take advantage - # of an Internet Explorer security flaw. - wfDebug( "$fname: getimagesize() doesn't recognize the file; rejecting.\n" ); - return false; - } + #check mime type, if desired + global $wgVerifyMimeType; + if ($wgVerifyMimeType) { + + #check mime type against file extension + if( !$this->verifyExtension( $mime, $extension ) ) { + return new WikiErrorMsg( 'uploadcorrupt' ); + } - $imageType = $data[2]; - if( !isset( $extensionTypes[$imageType] ) ) { - # Now we're kind of confused. Perhaps new image types added - # to PHP's support that we don't know about. - # We'll let these slide for now. - wfDebug( "$fname: getimagesize() knows the file, but we don't recognize the type; passing.\n" ); - return true; + #check mime type blacklist + global $wgMimeTypeBlacklist; + if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) + && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { + return new WikiErrorMsg( 'badfiletype', htmlspecialchars( $mime ) ); + } + } + + #check for htmlish code and javascript + if( $this->detectScript ( $tmpfile, $mime ) ) { + return new WikiErrorMsg( 'uploadscripted' ); } - $ext = strtolower( $extension ); - if( $extension != $extensionTypes[$imageType] ) { - # The given filename extension doesn't match the - # file type. Probably just a mistake, but it's a stupid - # one and we shouldn't let it pass. KILL THEM! - wfDebug( "$fname: file extension does not match recognized type; rejecting.\n" ); - return false; + /** + * Scan the uploaded file for viruses + */ + $virus= $this->detectVirus($tmpfile); + if ( $virus ) { + return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) ); } wfDebug( "$fname: all clear; passing.\n" ); @@ -727,66 +700,219 @@ class UploadForm { } /** - * Internet Explorer for Windows performs some really stupid file type - * autodetection which can cause it to interpret valid image files as HTML - * and potentially execute JavaScript, creating a cross-site scripting - * attack vectors. + * Checks if the mime type of the uploaded file matches the file extension. * - * Returns true if IE is likely to mistake the given file for HTML. - * - * @param string $filename + * @param string $mime the mime type of the uploaded file + * @param string $extension The filename extension that the file is to be served with * @return bool */ - function triggersIEbug( $filename ) { - $file = fopen( $filename, 'rb' ); - $chunk = strtolower( fread( $file, 256 ) ); - fclose( $file ); + function verifyExtension( $mime, $extension ) { + $fname = 'SpecialUpload::verifyExtension'; + + if (!$mime || $mime=="unknown" || $mime=="unknown/unknown") { + wfDebug( "$fname: passing file with unknown mime type\n" ); + return true; + } + + $magic=& wfGetMimeMagic(); + + $match= $magic->isMatchingExtension($extension,$mime); + + if ($match===NULL) { + wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" ); + return true; + } elseif ($match===true) { + wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" ); + + #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! + return true; + + } else { + wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" ); + return false; + } + } + + /** Heuristig for detecting files that *could* contain JavaScript instructions or + * things that may look like HTML to a browser and are thus + * potentially harmful. The present implementation will produce false positives in some situations. + * + * @param string $file Pathname to the temporary upload file + * @param string $mime The mime type of the file + * @return bool true if the file contains something looking like embedded scripts + */ + function detectScript($file,$mime) { + + #ugly hack: for text files, always look at the entire file. + #For binarie field, just check the first K. + + if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file ); + else { + $fp = fopen( $file, 'rb' ); + $chunk = fread( $fp, 1024 ); + fclose( $fp ); + } + + $chunk= strtolower( $chunk ); + + if (!$chunk) return false; + + #decode from UTF-16 if needed (could be used for obfuscation). + if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE"; + elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE"; + else $enc= NULL; + + if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk); + + $chunk= trim($chunk); + + #FIXME: convert from UTF-16 if necessarry! + + wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n"); + + #check for HTML doctype + if (eregi("addHTML( "
Bad configuration: unknown virus scanner: $wgAntivirus
\n" ); #LOCALIZE + + return "unknown antivirus: $wgAntivirus"; + } - $tags = array( - '&1",$output,$code); + + $exit_code= $code; #remeber for user feedback + + if ($virus_scanner_codes) { #map exit code to AV_xxx constants. + if (isset($virus_scanner_codes[$code])) $code= $virus_scanner_codes[$code]; #explicite mapping + else if (isset($virus_scanner_codes["*"])) $code= $virus_scanner_codes["*"]; #fallback mapping + } + + if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes) + wfDebug("$fname: failed to scan $file (code $exit_code).\n"); + + if ($wgAntivirusRequired) return "scan failed (code $exit_code)"; + else return NULL; + } + else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune) + wfDebug("$fname: unsupported file type $file (code $exit_code).\n"); + return NULL; + } + else if ($code===AV_NO_VIRUS) { + wfDebug("$fname: file passed virus scan.\n"); + return false; #no virus found + } + else { + $output= join("\n",$output); + $output= trim($output); + + if (!$output) $output= true; #if ther's no output, return true + else if ($msg_pattern) { + $groups= array(); + if (preg_match($msg_pattern,$output,$groups)) { + if ($groups[1]) $output= $groups[1]; + } } + + wfDebug("$fname: FOUND VIRUS! scanner feedback: $output"); + return $output; } - return false; } + } ?> diff --git a/includes/StreamFile.php b/includes/StreamFile.php index ae993d80f0..e154d6a202 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -25,141 +25,39 @@ does not.

} } + header( 'Content-Length: ' . $stat['size'] ); + $type = wfGetType( $fname ); - if ( $type ) { + if ( $type and $type!="unknown/unknown") { header("Content-type: $type"); } else { header('Content-type: application/x-wiki'); } + readfile( $fname ); } function wfGetType( $filename ) { - # There's probably a better way to do this - $types = <<guessMimeType($filename); //full fancy mime detection } - return false; } ?> diff --git a/includes/mime.info b/includes/mime.info new file mode 100644 index 0000000000..b314be0db1 --- /dev/null +++ b/includes/mime.info @@ -0,0 +1,76 @@ +#Mime type info file. +#the first mime type in each line is the "main" mime type, +#the others are aliases for this type +#the media type is given in upper case and square brackets, +#like [BITMAP], and must indicate a media type as defined by +#the MEDIATYPE_xxx constants in Defines.php + + +image/gif [BITMAP] +image/png [BITMAP] +image/ief [BITMAP] +image/jpeg [BITMAP] +image/xbm [BITMAP] +image/tiff [BITMAP] +image/x-icon [BITMAP] +image/x-rgb [BITMAP] +image/x-portable-pixmap [BITMAP] +image/x-portable-graymap image/x-portable-greymap [BITMAP] +image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP] +image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP] + +image/svg image/svg+xml application/svg+xml application/svg [DRAWING] +application/postscript [DRAWING] +application/x-latex [DRAWING] +application/x-tex [DRAWING] + + +audio/mp3 audio/mpeg3 audio/mpeg [AUDIO] +audio/wav audio/x-wav audio/wave [AUDIO] +audio/mid audio/midi [AUDIO] +audio/basic [AUDIO] +audio/x-aiff [AUDIO] +audio/x-pn-realaudio [AUDIO] +audio/x-realaudio [AUDIO] + +video/mpeg application/mpeg [VIDEO] +video/ogg [VIDEO] +video/x-sgi-video [VIDEO] + +application/ogg application/x-ogg audio/ogg audio/x-ogg video/ogg video/x-ogg [MULTIMEDIA] + +application/x-shockwave-flash [MULTIMEDIA] +audio/x-pn-realaudio-plugin [MULTIMEDIA] +model/iges [MULTIMEDIA] +model/mesh [MULTIMEDIA] +model/vrml [MULTIMEDIA] +video/quicktime [MULTIMEDIA] +video/x-msvideo [MULTIMEDIA] + +text/plain [TEXT] +text/html application/xhtml+xml [TEXT] +application/xml text/xml [TEXT] +text [TEXT] + +application/zip application/x-zip [ARCHIVE] +application/x-gzip [ARCHIVE] +application/x-bzip [ARCHIVE] +application/x-tar [ARCHIVE] +application/x-stuffit [ARCHIVE] + + +text/javascript application/x-javascript application/x-ecmascript text/ecmascript [EXECUTABLE] +application/x-bash [EXECUTABLE] +application/x-sh [EXECUTABLE] +application/x-csh [EXECUTABLE] +application/x-tcsh [EXECUTABLE] +application/x-tcl [EXECUTABLE] +application/x-perl [EXECUTABLE] +application/x-python [EXECUTABLE] + +application/pdf application/acrobat [OFFICE] +application/msword [OFFICE] +application/vnd.ms-excel [OFFICE] +application/vnd.ms-powerpoint [OFFICE] +application/x-director [OFFICE] +text/rtf [OFFICE] diff --git a/includes/mime.types b/includes/mime.types new file mode 100644 index 0000000000..3a7fa39cd5 --- /dev/null +++ b/includes/mime.types @@ -0,0 +1,117 @@ +application/andrew-inset ez +application/mac-binhex40 hqx +application/mac-compactpro cpt +application/mathml+xml mathml +application/msword doc +application/octet-stream bin dms lha lzh exe class so dll +application/oda oda +application/ogg ogg ogm +application/pdf pdf +application/postscript ai eps ps +application/rdf+xml rdf +application/smil smi smil +application/srgs gram +application/srgs+xml grxml +application/vnd.mif mif +application/vnd.ms-excel xls +application/vnd.ms-powerpoint ppt +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/voicexml+xml vxml +application/x-bcpio bcpio +application/x-bzip gz bz2 +application/x-cdlink vcd +application/x-chess-pgn pgn +application/x-cpio cpio +application/x-csh csh +application/x-director dcr dir dxr +application/x-dvi dvi +application/x-futuresplash spl +application/x-gtar gtar tar +application/x-gzip gz +application/x-hdf hdf +application/x-jar jar +application/x-javascript js +application/x-koan skp skd skt skm +application/x-latex latex +application/x-netcdf nc cdf +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-stuffit sit +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-texinfo texinfo texi +application/x-troff t tr roff +application/x-troff-man man +application/x-troff-me me +application/x-troff-ms ms +application/x-ustar ustar +application/x-wais-source src +application/x-xpinstall xpi +application/xhtml+xml xhtml xht +application/xslt+xml xslt +application/xml xml xsl +application/xml-dtd dtd +application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw +audio/basic au snd +audio/midi mid midi kar +audio/mpeg mpga mp2 mp3 +audio/ogg ogg +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-ogg ogg +audio/x-pn-realaudio ram rm +audio/x-pn-realaudio-plugin rpm +audio/x-realaudio ra +audio/x-wav wav +chemical/x-pdb pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +image/gif gif +image/ief ief +image/jpeg jpeg jpg jpe +image/png png +image/svg+xml svg +image/tiff tiff tif +image/vnd.djvu djvu djv +image/vnd.wap.wbmp wbmp +image/x-cmu-raster ras +image/x-icon ico +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-photoshop psd +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +model/iges igs iges +model/mesh msh mesh silo +model/vrml wrl vrml +text/calendar ics ifb +text/css css +text/html html htm +text/plain txt +text/richtext rtx +text/rtf rtf +text/sgml sgml sgm +text/tab-separated-values tsv +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/xml xml xsl xslt rss rdf +text/x-setext etx +video/mpeg mpeg mpg mpe +video/ogg ogm ogg +video/quicktime qt mov +video/vnd.mpegurl mxu +video/x-msvideo avi +video/x-ogg ogm ogg +video/x-sgi-movie movie +x-conference/x-cooltalk ice \ No newline at end of file diff --git a/maintenance/archives/patch-img_media_type.sql b/maintenance/archives/patch-img_media_type.sql new file mode 100644 index 0000000000..c9ef8548a7 --- /dev/null +++ b/maintenance/archives/patch-img_media_type.sql @@ -0,0 +1,20 @@ +-- media type columns, added for 1.5 +-- this alters the scheme for 1.5, img_type is no longer used. + +ALTER TABLE /*$wgDBprefix*/image ADD ( + -- Media type as defined by the MEDIATYPE_xxx constants + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + + -- major part of a MIME media type as defined by IANA + -- see http://www.iana.org/assignments/media-types/ + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + + -- minor part of a MIME media type as defined by IANA + -- the minor parts are not required to adher to any standard + -- but should be consistent throughout the database + -- see http://www.iana.org/assignments/media-types/ + img_minor_mime varchar(32) NOT NULL default "unknown" +); + +-- img_type is no longer used, delete it +ALTER TABLE /*$wgDBprefix*/image DROP COLUMN img_type; \ No newline at end of file diff --git a/maintenance/archives/patch-img_width.sql b/maintenance/archives/patch-img_width.sql index 9dc233f26e..a705dbdbb1 100644 --- a/maintenance/archives/patch-img_width.sql +++ b/maintenance/archives/patch-img_width.sql @@ -1,17 +1,20 @@ -- Extra image metadata, added for 1.5 +-- NOTE: as by patch-img_media_type.sql, the img_type +-- column is no longer used and has therefore be removed from this patch + ALTER TABLE /*$wgDBprefix*/image ADD ( img_width int(5) NOT NULL default 0, img_height int(5) NOT NULL default 0, img_bits int(5) NOT NULL default 0, - img_type int(5) NOT NULL default -1 + -- img_type int(5) NOT NULL default -1 ); ALTER TABLE /*$wgDBprefix*/oldimage ADD ( oi_width int(5) NOT NULL default 0, oi_height int(5) NOT NULL default 0, oi_bits int(3) NOT NULL default 0, - oi_type int(3) NOT NULL default 0 + -- oi_type int(3) NOT NULL default 0 ); diff --git a/maintenance/parserTests.php b/maintenance/parserTests.php index 9de5751da9..f17590fea1 100644 --- a/maintenance/parserTests.php +++ b/maintenance/parserTests.php @@ -423,7 +423,9 @@ class ParserTest { 'img_width' => 1941, 'img_height' => 220, 'img_bits' => 24, - 'img_type' => 2, // 2 == JPEG + 'img_media_type' => MEDIATYPE_BITMAP, + 'img_major_mime' => "image", + 'img_minor_mime' => "jpeg", ) ); $setupDB = true; diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 9926966ed9..919b822cc6 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -531,9 +531,18 @@ CREATE TABLE /*$wgDBprefix*/image ( -- For images, bits per pixel if known. img_bits int(3) NOT NULL default '0', - -- File type key returned by getimagesize(). - -- See http://www.php.net/getimagesize for possible values. - img_type int(3) NOT NULL default '0', + -- Media type as defined by the MEDIATYPE_xxx constants + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + + -- major part of a MIME media type as defined by IANA + -- see http://www.iana.org/assignments/media-types/ + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + + -- minor part of a MIME media type as defined by IANA + -- the minor parts are not required to adher to any standard + -- but should be consistent throughout the database + -- see http://www.iana.org/assignments/media-types/ + img_minor_mime varchar(32) NOT NULL default "unknown", -- Description field as entered by the uploader. -- This is displayed in image upload history and logs. @@ -574,7 +583,6 @@ CREATE TABLE /*$wgDBprefix*/oldimage ( oi_width int(5) NOT NULL default 0, oi_height int(5) NOT NULL default 0, oi_bits int(3) NOT NULL default 0, - oi_type int(3) NOT NULL default 0, oi_description tinyblob NOT NULL default '', oi_user int(5) unsigned NOT NULL default '0', oi_user_text varchar(255) binary NOT NULL default '', diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 4399f520c1..ce2935e26e 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -46,6 +46,7 @@ $wgNewFields = array( array( 'revision', 'rev_deleted', 'patch-rev_deleted.sql' ), array( 'image', 'img_width', 'patch-img_width.sql' ), array( 'image', 'img_metadata', 'patch-img_metadata.sql' ), + array( 'image', 'img_media_type', 'patch-img_media_type.sql' ), ); function rename_table( $from, $to, $patch ) { diff --git a/thumb.php b/thumb.php index 73d2ca9e4b..ff84d89468 100644 --- a/thumb.php +++ b/thumb.php @@ -12,6 +12,9 @@ $wgNoOutputBuffer = true; require_once( './includes/Defines.php' ); require_once( './LocalSettings.php' ); require_once( 'GlobalFunctions.php' ); + +$wgTrivialMimeDetection = true; //don't use fancy mime detection, just check the file extension for jpg/gif/png. + require_once( 'Image.php' ); require_once( 'StreamFile.php' ); @@ -25,6 +28,8 @@ if ( get_magic_quotes_gpc() ) { $width = $_REQUEST['w']; } +$pre_render= isset($_REQUEST['r']) && $_REQUEST['r']!="0"; + // Some basic input validation $width = intval( $width ); @@ -34,7 +39,7 @@ $fileName = strtr( $fileName, '\\/', '__' ); $imagePath = wfImageDir( $fileName ) . '/' . $fileName; $thumbName = "{$width}px-$fileName"; -if ( preg_match( '/\.svg$/', $fileName ) ) { +if ( $pre_render ) { $thumbName .= '.png'; } $thumbPath = wfImageThumbDir( $fileName ) . '/' . $thumbName;