]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/UploadBase.php
MediaWiki 1.14.0
[autoinstallsdev/mediawiki.git] / includes / UploadBase.php
1 <?php
2
3 class UploadBase {
4         var $mTempPath;
5         var $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
6         var $mTitle = false, $mTitleError = 0;
7         var $mFilteredName, $mFinalExtension;
8         
9         const SUCCESS = 0;
10         const OK = 0;
11         const BEFORE_PROCESSING = 1;
12         const LARGE_FILE_SERVER = 2;
13         const EMPTY_FILE = 3;
14         const MIN_LENGTH_PARTNAME = 4;
15         const ILLEGAL_FILENAME = 5;
16         const PROTECTED_PAGE = 6;
17         const OVERWRITE_EXISTING_FILE = 7;
18         const FILETYPE_MISSING = 8;
19         const FILETYPE_BADTYPE = 9;
20         const VERIFICATION_ERROR = 10;
21         const UPLOAD_VERIFICATION_ERROR = 11;
22         const UPLOAD_WARNING = 12;
23         const INTERNAL_ERROR = 13;
24         
25         const SESSION_VERSION = 2;
26         
27         /**
28          * Returns true if uploads are enabled.
29          * Can be overriden by subclasses.
30          */
31         static function isEnabled() {
32                 global $wgEnableUploads;
33                 return $wgEnableUploads;
34         }
35         /**
36          * Returns true if the user can use this upload module or else a string 
37          * identifying the missing permission.
38          * Can be overriden by subclasses.
39          */
40         static function isAllowed( $user ) {
41                 if( !$user->isAllowed( 'upload' ) )
42                         return 'upload';
43                 return true;
44         }
45         
46         // Upload handlers. Should probably just be a global
47         static $uploadHandlers = array( 'Stash', 'Upload', 'Url' );
48         /**
49          * Create a form of UploadBase depending on wpSourceType and initializes it
50          */
51         static function createFromRequest( &$request, $type = null ) {
52                 $type = $type ? $type : $request->getVal( 'wpSourceType' );
53                 if( !$type ) 
54                         return null;
55                 $type = ucfirst($type);
56                 $className = 'UploadFrom'.$type;
57                 if( !in_array( $type, self::$uploadHandlers ) )
58                         return null;
59                 if( !call_user_func( array( $className, 'isEnabled' ) ) )
60                         return null;
61                 if( !call_user_func( array( $className, 'isValidRequest' ), $request ) )
62                         return null;
63                 
64                 $handler = new $className;
65                 $handler->initializeFromRequest( $request );
66                 return $handler;
67         }
68         
69         /**
70          * Check whether a request if valid for this handler
71          */
72         static function isValidRequest( $request ) {
73                 return false;
74         }
75         
76         function __construct() {}
77         
78         /**
79          * Do the real variable initialization
80          */
81         function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) {
82                 $this->mDesiredDestName = $name;
83                 $this->mTempPath = $tempPath;
84                 $this->mFileSize = $fileSize;
85                 $this->mRemoveTempFile = $removeTempFile;
86         }
87
88         /**
89          * Fetch the file. Usually a no-op
90          */
91         function fetchFile() {
92                 return self::OK;
93         }
94
95         /**
96          * Verify whether the upload is sane. 
97          * Returns self::OK or else an array with error information
98          */
99         function verifyUpload() {
100                 global $wgUser;
101                 
102                 /**
103                  * If there was no filename or a zero size given, give up quick.
104                  */
105                 if( empty( $this->mFileSize ) ) 
106                         return array( 'status' => self::EMPTY_FILE );
107
108                 $nt = $this->getTitle();
109                 if( is_null( $nt ) ) {
110                         $result = array( 'status' => $this->mTitleError );
111                         if( $this->mTitleError == self::ILLEGAL_FILENAME )
112                                 $resul['filtered'] = $this->mFilteredName;
113                         if ( $this->mTitleError == self::FILETYPE_BADTYPE )
114                                 $result['finalExt'] = $this->mFinalExtension;
115                         return $result;
116                 }
117                 $this->mLocalFile = wfLocalFile( $nt );
118                 $this->mDestName = $this->mLocalFile->getName();
119
120                 /**
121                  * In some cases we may forbid overwriting of existing files.
122                  */
123                 $overwrite = $this->checkOverwrite( $this->mDestName );
124                 if( $overwrite !== true )
125                         return array( 'status' => self::OVERWRITE_EXISTING_FILE, 'overwrite' => $overwrite );
126                 
127                 /**
128                  * Look at the contents of the file; if we can recognize the
129                  * type but it's corrupt or data of the wrong type, we should
130                  * probably not accept it.
131                  */
132                 $verification = $this->verifyFile( $this->mTempPath );
133
134                 if( $verification !== true ) {
135                         if( !is_array( $verification ) ) 
136                                 $verification = array( $verification );
137                         $verification['status'] = self::VERIFICATION_ERROR;
138                         return $verification;
139                 }
140                 
141                 $error = '';
142                 if( !wfRunHooks( 'UploadVerification',
143                                 array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
144                         return array( 'status' => self::UPLOAD_VERIFICATION_ERROR, 'error' => $error );
145                 }
146                 
147                 return self::OK;
148         }
149         
150         /**
151          * Verifies that it's ok to include the uploaded file
152          *
153          * @param string $tmpfile the full path of the temporary file to verify
154          * @return mixed true of the file is verified, a string or array otherwise.
155          */
156         protected function verifyFile( $tmpfile ) {
157                 $this->mFileProps = File::getPropsFromPath( $this->mTempPath, 
158                 $this->mFinalExtension );
159                 $this->checkMacBinary();
160                 
161                 #magically determine mime type
162                 $magic = MimeMagic::singleton();
163                 $mime = $magic->guessMimeType( $tmpfile, false );
164
165                 #check mime type, if desired
166                 global $wgVerifyMimeType;
167                 if ( $wgVerifyMimeType ) {
168
169                   wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
170                         #check mime type against file extension
171                         if( !self::verifyExtension( $mime, $this->mFinalExtension ) ) {
172                                 return 'uploadcorrupt';
173                         }
174
175                         #check mime type blacklist
176                         global $wgMimeTypeBlacklist;
177                         if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
178                                 && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
179                                 return array( 'filetype-badmime', $mime );
180                         }
181                 }
182
183                 #check for htmlish code and javascript
184                 if( $this->detectScript ( $tmpfile, $mime, $this->mFinalExtension ) ) {
185                         return 'uploadscripted';
186                 }
187
188                 /**
189                 * Scan the uploaded file for viruses
190                 */
191                 $virus = $this->detectVirus($tmpfile);
192                 if ( $virus ) {
193                         return array( 'uploadvirus', $virus );
194                 }
195
196                 wfDebug( __METHOD__.": all clear; passing.\n" );
197                 return true;
198         }
199         
200         /**
201          * Check whether the user can edit, upload and create the image
202          */
203         function verifyPermissions( $user ) {
204                 /**
205                  * If the image is protected, non-sysop users won't be able
206                  * to modify it by uploading a new revision.
207                  */
208                 $nt = $this->getTitle();
209                 if( is_null( $nt ) )
210                         return true;
211                 $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
212                 $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
213                 $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) );
214                 if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
215                         $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
216                         $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
217                         return $permErrors;
218                 }
219                 return true;
220         }       
221         
222         /**
223          * Check for non fatal problems with the file
224          */
225         function checkWarnings() {
226                 $warning = array();
227
228                 $filename = $this->mLocalFile->getName();
229                 $n = strrpos( $filename, '.' );         
230                 $partname = $n ? substr( $filename, 0, $n ) : $filename;
231
232                 // Check whether the resulting filename is different from the desired one
233                 if( $this->mDesiredDestName != $filename )
234                         $warning['badfilename'] = $filename;
235
236                 // Check whether the file extension is on the unwanted list
237                 global $wgCheckFileExtensions, $wgFileExtensions;
238                 if ( $wgCheckFileExtensions ) {
239                         if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
240                                 $warning['filetype-unwanted-type'] = $this->mFinalExtension; 
241                 }
242                 
243                 global $wgUploadSizeWarning;
244                 if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) )
245                         $warning['large-file'] = $wgUploadSizeWarning;
246
247                 if ( $this->mFileSize == 0 )
248                         $warning['emptyfile'] = true;
249                 
250                 $exists = self::getExistsWarning( $this->mLocalFile );
251                 if( $exists !== false )
252                         $warning['exists'] = $exists;
253                 
254                 // Check whether this may be a thumbnail
255                 if( $exists !== false && $exists[0] != 'thumb' 
256                                 && self::isThumbName( $this->mLocalFile->getName() ) )
257                         $warning['file-thumbnail-no'] = substr( $filename , 0, 
258                                 strpos( $nt->getText() , '-' ) +1 );
259                 
260                 $hash = File::sha1Base36( $this->mTempPath );
261                 $dupes = RepoGroup::singleton()->findBySha1( $hash );
262                 if( $dupes )
263                         $warning['duplicate'] = $dupes;
264                         
265                 $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
266                 foreach( $filenamePrefixBlacklist as $prefix ) {
267                         if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
268                                 $warning['filename-bad-prefix'] = $prefix;
269                                 break;
270                         }
271                 }
272                 
273                 # If the file existed before and was deleted, warn the user of this
274                 # Don't bother doing so if the file exists now, however
275                 if( $this->mLocalFile->wasDeleted() && !$this->mLocalFile->exists() )
276                         $warning['filewasdeleted'] = $this->mLocalFile->getTitle();
277                         
278                 return $warning;
279         }
280
281         /**
282          * Really perform the upload.
283          */
284         function performUpload( $comment, $pageText, $watch, $user ) {
285                 $status = $this->mLocalFile->upload( $this->mTempPath, $comment, $pageText,
286                         File::DELETE_SOURCE, $this->mFileProps, false, $user );
287                 
288                 if( $status->isGood() && $watch ) {
289                         $user->addWatch( $this->mLocalFile->getTitle() );
290                 }
291                 
292                 if( $status->isGood() )
293                         wfRunHooks( 'UploadComplete', array( &$this ) );
294                 
295                 return $status;
296         }
297
298         /**
299          * Returns a title or null
300          */
301         function getTitle() {
302                 if ( $this->mTitle !== false )
303                         return $this->mTitle;
304                 
305                 /**
306                  * Chop off any directories in the given filename. Then
307                  * filter out illegal characters, and try to make a legible name
308                  * out of it. We'll strip some silently that Title would die on.
309                  */
310
311                 $basename = $this->mDesiredDestName;
312
313                 $this->mFilteredName = wfStripIllegalFilenameChars( $basename );
314
315                 /**
316                  * We'll want to blacklist against *any* 'extension', and use
317                  * only the final one for the whitelist.
318                  */
319                 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
320
321                 if( count( $ext ) ) {
322                         $this->mFinalExtension = $ext[count( $ext ) - 1];
323                 } else {
324                         $this->mFinalExtension = '';
325                 }
326
327                 /* Don't allow users to override the blacklist (check file extension) */
328                 global $wgCheckFileExtensions, $wgStrictFileExtensions;
329                 global $wgFileExtensions, $wgFileBlacklist;
330                 if ( $this->mFinalExtension == '' ) {
331                         $this->mTitleError = self::FILETYPE_MISSING;
332                         return $this->mTitle = null;
333                 } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
334                                 ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
335                                         !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
336                         $this->mTitleError = self::FILETYPE_BADTYPE;
337                         return $this->mTitle = null;
338                 }
339
340                 # If there was more than one "extension", reassemble the base
341                 # filename to prevent bogus complaints about length
342                 if( count( $ext ) > 1 ) {
343                         for( $i = 0; $i < count( $ext ) - 1; $i++ )
344                                 $partname .= '.' . $ext[$i];
345                 }
346
347                 if( strlen( $partname ) < 1 ) {
348                         $this->mTitleError =  self::MIN_LENGTH_PARTNAME;
349                         return $this->mTitle = null;
350                 }
351                 
352                 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
353                 if( is_null( $nt ) ) {
354                         $this->mTitleError = self::ILLEGAL_FILENAME;
355                         return $this->mTitle = null;
356                 }
357                 return $this->mTitle = $nt;
358         }
359         
360         function getLocalFile() {
361                 if( is_null( $this->mLocalFile ) ) {
362                         $nt = $this->getTitle();
363                         $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
364                 }       
365                 return $this->mLocalFile;
366         }
367         
368         /**
369          * Stash a file in a temporary directory for later processing
370          * after the user has confirmed it.
371          *
372          * If the user doesn't explicitly cancel or accept, these files
373          * can accumulate in the temp directory.
374          *
375          * @param string $saveName - the destination filename
376          * @param string $tempName - the source temporary file to save
377          * @return string - full path the stashed file, or false on failure
378          * @access private
379          */
380         function saveTempUploadedFile( $saveName, $tempName ) {
381                 global $wgOut;
382                 $repo = RepoGroup::singleton()->getLocalRepo();
383                 $status = $repo->storeTemp( $saveName, $tempName );
384                 return $status;
385         }
386         
387         /**
388          * Stash a file in a temporary directory for later processing,
389          * and save the necessary descriptive info into the session.
390          * Returns a key value which will be passed through a form
391          * to pick up the path info on a later invocation.
392          *
393          * @return int
394          * @access private
395          */
396         function stashSession() {
397                 $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
398
399                 if( !$status->isGood() ) {
400                         # Couldn't save the file.
401                         return false;
402                 }
403
404                 return array(
405                         'mTempPath'       => $status->value,
406                         'mFileSize'       => $this->mFileSize,
407                         'mFileProps'      => $this->mFileProps,
408                         'version'         => self::SESSION_VERSION,
409                 );
410         }
411         
412         /**
413          * Remove a temporarily kept file stashed by saveTempUploadedFile().
414          * @return success
415          */
416         function unsaveUploadedFile() {
417                 $repo = RepoGroup::singleton()->getLocalRepo();
418                 $success = $repo->freeTemp( $this->mTempPath );
419                 return $success;
420         }
421         
422         /**
423          * If we've modified the upload file we need to manually remove it
424          * on exit to clean up.
425          * @access private
426          */
427         function cleanupTempFile() {
428                 if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
429                         wfDebug( __METHOD__.": Removing temporary file {$this->mTempPath}\n" );
430                         unlink( $this->mTempPath );
431                 }
432         }
433         
434         function getTempPath() {
435                 return $this->mTempPath;
436         }
437         
438         
439                 /**
440          * Split a file into a base name and all dot-delimited 'extensions'
441          * on the end. Some web server configurations will fall back to
442          * earlier pseudo-'extensions' to determine type and execute
443          * scripts, so the blacklist needs to check them all.
444          *
445          * @return array
446          */
447         function splitExtensions( $filename ) {
448                 $bits = explode( '.', $filename );
449                 $basename = array_shift( $bits );
450                 return array( $basename, $bits );
451         }
452
453         /**
454          * Perform case-insensitive match against a list of file extensions.
455          * Returns true if the extension is in the list.
456          *
457          * @param string $ext
458          * @param array $list
459          * @return bool
460          */
461         function checkFileExtension( $ext, $list ) {
462                 return in_array( strtolower( $ext ), $list );
463         }
464
465         /**
466          * Perform case-insensitive match against a list of file extensions.
467          * Returns true if any of the extensions are in the list.
468          *
469          * @param array $ext
470          * @param array $list
471          * @return bool
472          */
473         function checkFileExtensionList( $ext, $list ) {
474                 foreach( $ext as $e ) {
475                         if( in_array( strtolower( $e ), $list ) ) {
476                                 return true;
477                         }
478                 }
479                 return false;
480         }
481         
482         
483         /**
484          * Checks if the mime type of the uploaded file matches the file extension.
485          *
486          * @param string $mime the mime type of the uploaded file
487          * @param string $extension The filename extension that the file is to be served with
488          * @return bool
489          */
490         public static function verifyExtension( $mime, $extension ) {
491                 $magic = MimeMagic::singleton();
492
493                 if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
494                         if ( ! $magic->isRecognizableExtension( $extension ) ) {
495                                 wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
496                                         "unrecognized extension '$extension', can't verify\n" );
497                                 return true;
498                         } else {
499                                 wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
500                                         "recognized extension '$extension', so probably invalid file\n" );
501                                 return false;
502                         }
503
504                 $match= $magic->isMatchingExtension($extension,$mime);
505
506                 if ($match===NULL) {
507                         wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
508                         return true;
509                 } elseif ($match===true) {
510                         wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
511
512                         #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
513                         return true;
514
515                 } else {
516                         wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
517                         return false;
518                 }
519         }
520
521         /**
522          * Heuristic for detecting files that *could* contain JavaScript instructions or
523          * things that may look like HTML to a browser and are thus
524          * potentially harmful. The present implementation will produce false positives in some situations.
525          *
526          * @param string $file Pathname to the temporary upload file
527          * @param string $mime The mime type of the file
528          * @param string $extension The extension of the file
529          * @return bool true if the file contains something looking like embedded scripts
530          */
531         function detectScript($file, $mime, $extension) {
532                 global $wgAllowTitlesInSVG;
533
534                 #ugly hack: for text files, always look at the entire file.
535                 #For binary field, just check the first K.
536
537                 if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
538                 else {
539                         $fp = fopen( $file, 'rb' );
540                         $chunk = fread( $fp, 1024 );
541                         fclose( $fp );
542                 }
543
544                 $chunk= strtolower( $chunk );
545
546                 if (!$chunk) return false;
547
548                 #decode from UTF-16 if needed (could be used for obfuscation).
549                 if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
550                 elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
551                 else $enc= NULL;
552
553                 if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
554
555                 $chunk= trim($chunk);
556
557                 #FIXME: convert from UTF-16 if necessarry!
558
559                 wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
560
561                 #check for HTML doctype
562                 if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
563
564                 /**
565                 * Internet Explorer for Windows performs some really stupid file type
566                 * autodetection which can cause it to interpret valid image files as HTML
567                 * and potentially execute JavaScript, creating a cross-site scripting
568                 * attack vectors.
569                 *
570                 * Apple's Safari browser also performs some unsafe file type autodetection
571                 * which can cause legitimate files to be interpreted as HTML if the
572                 * web server is not correctly configured to send the right content-type
573                 * (or if you're really uploading plain text and octet streams!)
574                 *
575                 * Returns true if IE is likely to mistake the given file for HTML.
576                 * Also returns true if Safari would mistake the given file for HTML
577                 * when served with a generic content-type.
578                 */
579
580                 $tags = array(
581                         '<body',
582                         '<head',
583                         '<html',   #also in safari
584                         '<img',
585                         '<pre',
586                         '<script', #also in safari
587                         '<table'
588                         );
589                 if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
590                         $tags[] = '<title';
591                 }
592
593                 foreach( $tags as $tag ) {
594                         if( false !== strpos( $chunk, $tag ) ) {
595                                 return true;
596                         }
597                 }
598
599                 /*
600                 * look for javascript
601                 */
602
603                 #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
604                 $chunk = Sanitizer::decodeCharReferences( $chunk );
605
606                 #look for script-types
607                 if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true;
608
609                 #look for html-style script-urls
610                 if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
611
612                 #look for css-style script-urls
613                 if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
614
615                 wfDebug("SpecialUpload::detectScript: no scripts found\n");
616                 return false;
617         }
618
619         /**
620          * Generic wrapper function for a virus scanner program.
621          * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
622          * $wgAntivirusRequired may be used to deny upload if the scan fails.
623          *
624          * @param string $file Pathname to the temporary upload file
625          * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
626          *         or a string containing feedback from the virus scanner if a virus was found.
627          *         If textual feedback is missing but a virus was found, this function returns true.
628          */
629         function detectVirus($file) {
630                 global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
631
632                 if ( !$wgAntivirus ) {
633                         wfDebug( __METHOD__.": virus scanner disabled\n");
634                         return NULL;
635                 }
636
637                 if ( !$wgAntivirusSetup[$wgAntivirus] ) {
638                         wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
639                         $wgOut->wrapWikiMsg( '<div class="error">$1</div>', array( 'virus-badscanner', $wgAntivirus ) );
640                         return wfMsg('virus-unknownscanner') . " $wgAntivirus";
641                 }
642
643                 # look up scanner configuration
644                 $command = $wgAntivirusSetup[$wgAntivirus]["command"];
645                 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
646                 $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
647                         $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
648
649                 if ( strpos( $command,"%f" ) === false ) {
650                         # simple pattern: append file to scan
651                         $command .= " " . wfEscapeShellArg( $file );
652                 } else {
653                         # complex pattern: replace "%f" with file to scan
654                         $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
655                 }
656
657                 wfDebug( __METHOD__.": running virus scan: $command \n" );
658
659                 # execute virus scanner
660                 $exitCode = false;
661
662                 #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
663                 #      that does not seem to be worth the pain.
664                 #      Ask me (Duesentrieb) about it if it's ever needed.
665                 $output = array();
666                 if ( wfIsWindows() ) {
667                         exec( "$command", $output, $exitCode );
668                 } else {
669                         exec( "$command 2>&1", $output, $exitCode );
670                 }
671
672                 # map exit code to AV_xxx constants.
673                 $mappedCode = $exitCode;
674                 if ( $exitCodeMap ) {
675                         if ( isset( $exitCodeMap[$exitCode] ) ) {
676                                 $mappedCode = $exitCodeMap[$exitCode];
677                         } elseif ( isset( $exitCodeMap["*"] ) ) {
678                                 $mappedCode = $exitCodeMap["*"];
679                         }
680                 }
681
682                 if ( $mappedCode === AV_SCAN_FAILED ) {
683                         # scan failed (code was mapped to false by $exitCodeMap)
684                         wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
685
686                         if ( $wgAntivirusRequired ) {
687                                 return wfMsg('virus-scanfailed', array( $exitCode ) );
688                         } else {
689                                 return NULL;
690                         }
691                 } else if ( $mappedCode === AV_SCAN_ABORTED ) {
692                         # scan failed because filetype is unknown (probably imune)
693                         wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
694                         return NULL;
695                 } else if ( $mappedCode === AV_NO_VIRUS ) {
696                         # no virus found
697                         wfDebug( __METHOD__.": file passed virus scan.\n" );
698                         return false;
699                 } else {
700                         $output = join( "\n", $output );
701                         $output = trim( $output );
702
703                         if ( !$output ) {
704                                 $output = true; #if there's no output, return true
705                         } elseif ( $msgPattern ) {
706                                 $groups = array();
707                                 if ( preg_match( $msgPattern, $output, $groups ) ) {
708                                         if ( $groups[1] ) {
709                                                 $output = $groups[1];
710                                         }
711                                 }
712                         }
713
714                         wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
715                         return $output;
716                 }
717         }
718
719         /**
720          * Check if the temporary file is MacBinary-encoded, as some uploads
721          * from Internet Explorer on Mac OS Classic and Mac OS X will be.
722          * If so, the data fork will be extracted to a second temporary file,
723          * which will then be checked for validity and either kept or discarded.
724          *
725          * @access private
726          */
727         function checkMacBinary() {
728                 $macbin = new MacBinary( $this->mTempPath );
729                 if( $macbin->isValid() ) {
730                         $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
731                         $dataHandle = fopen( $dataFile, 'wb' );
732
733                         wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
734                         $macbin->extractData( $dataHandle );
735
736                         $this->mTempPath = $dataFile;
737                         $this->mFileSize = $macbin->dataForkLength();
738
739                         // We'll have to manually remove the new file if it's not kept.
740                         $this->mRemoveTempFile = true;
741                 }
742                 $macbin->close();
743         }
744         
745         /**
746          * Check if there's an overwrite conflict and, if so, if restrictions
747          * forbid this user from performing the upload.
748          *
749          * @return mixed true on success, WikiError on failure
750          * @access private
751          */
752         function checkOverwrite() {
753                 global $wgUser;
754                 // First check whether the local file can be overwritten
755                 if( $this->mLocalFile->exists() )
756                         if( !self::userCanReUpload( $wgUser, $this->mLocalFile ) )
757                                 return 'fileexists-forbidden';
758                 
759                 // Check shared conflicts
760                 $file = wfFindFile( $this->mLocalFile->getName() );
761                 if ( $file && ( !$wgUser->isAllowed( 'reupload' ) ||
762                                 !$wgUser->isAllowed( 'reupload-shared' ) ) )
763                         return 'fileexists-shared-forbidden';
764                 
765                 return true;
766                                   
767         }
768         
769         /**
770          * Check if a user is the last uploader
771          *
772          * @param User $user
773          * @param string $img, image name
774          * @return bool
775          */
776         public static function userCanReUpload( User $user, $img ) {
777                 if( $user->isAllowed( 'reupload' ) )
778                         return true; // non-conditional
779                 if( !$user->isAllowed( 'reupload-own' ) )
780                         return false;
781                 if( is_string( $img ) )
782                         $img = wfLocalFile( $img );
783                 if ( !( $img instanceof LocalFile ) )
784                         return false;
785
786                 return $user->getId() == $img->getUser( 'id' );
787         }
788         
789         public static function getExistsWarning( $file ) {
790                 if( $file->exists() )
791                         return array( 'exists', $file );
792                 
793                 if( $file->getTitle()->getArticleID() )
794                         return array( 'page-exists', $file );
795                 
796                 if( strpos( $file->getName(), '.' ) == false ) {
797                         $partname = $file->getName();
798                         $rawExtension = '';
799                 } else {
800                         $n = strrpos( $file->getName(), '.' );
801                         $rawExtension = substr( $file->getName(), $n + 1 );
802                         $partname = substr( $file->getName(), 0, $n );
803                 }
804                 
805                 if ( $rawExtension != $file->getExtension() ) {
806                         // We're not using the normalized form of the extension.
807                         // Normal form is lowercase, using most common of alternate
808                         // extensions (eg 'jpg' rather than 'JPEG').
809                         //
810                         // Check for another file using the normalized form...
811                         $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() );
812                         $file_lc = wfLocalFile( $nt_lc );
813                         
814                         if( $file_lc->exists() )
815                                 return array( 'exists-normalized', $file_lc );
816                 } 
817                 
818                 if ( self::isThumbName( $file->getName() ) ) {
819                         # Check for filenames like 50px- or 180px-, these are mostly thumbnails
820                         $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
821                         $file_thb = wfLocalFile( $nt_thb );
822                         if( $file_thb->exists() )
823                                 return array( 'thumb', $file_thb );
824                 }
825                 
826                 return false;
827         }
828         
829         public static function isThumbName( $filename ) {
830                 $n = strrpos( $filename, '.' );
831                 $partname = $n ? substr( $filename, 0, $n ) : $filename;
832                 return ( 
833                                         substr( $partname , 3, 3 ) == 'px-' || 
834                                         substr( $partname , 2, 3 ) == 'px-' 
835                                 ) && 
836                                 ereg( "[0-9]{2}" , substr( $partname , 0, 2) ); 
837         }
838         
839         /**
840          * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
841          *
842          * @return array list of prefixes
843          */
844         public static function getFilenamePrefixBlacklist() {
845                 $blacklist = array();
846                 $message = wfMsgForContent( 'filename-prefix-blacklist' );
847                 if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
848                         $lines = explode( "\n", $message );
849                         foreach( $lines as $line ) {
850                                 // Remove comment lines
851                                 $comment = substr( trim( $line ), 0, 1 );
852                                 if ( $comment == '#' || $comment == '' ) {
853                                         continue;
854                                 }
855                                 // Remove additional comments after a prefix
856                                 $comment = strpos( $line, '#' );
857                                 if ( $comment > 0 ) {
858                                         $line = substr( $line, 0, $comment-1 );
859                                 }
860                                 $blacklist[] = trim( $line );
861                         }
862                 }
863                 return $blacklist;
864         }
865         
866         
867 }