]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/SpecialUpload.php
MediaWiki 1.11.0
[autoinstallsdev/mediawiki.git] / includes / SpecialUpload.php
1 <?php
2 /**
3  *
4  * @addtogroup SpecialPage
5  */
6
7
8 /**
9  * Entry point
10  */
11 function wfSpecialUpload() {
12         global $wgRequest;
13         $form = new UploadForm( $wgRequest );
14         $form->execute();
15 }
16
17 /**
18  * implements Special:Upload
19  * @addtogroup SpecialPage
20  */
21 class UploadForm {
22         /**#@+
23          * @access private
24          */
25         var $mComment, $mLicense, $mIgnoreWarning, $mCurlError;
26         var $mDestName, $mTempPath, $mFileSize, $mFileProps;
27         var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked;
28         var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType;
29         var $mDestWarningAck, $mCurlDestHandle;
30         var $mLocalFile;
31
32         # Placeholders for text injection by hooks (must be HTML)
33         # extensions should take care to _append_ to the present value
34         var $uploadFormTextTop;
35         var $uploadFormTextAfterSummary;
36
37         const SESSION_VERSION = 1;
38         /**#@-*/
39
40         /**
41          * Constructor : initialise object
42          * Get data POSTed through the form and assign them to the object
43          * @param $request Data posted.
44          */
45         function UploadForm( &$request ) {
46                 global $wgAllowCopyUploads;
47                 $this->mDesiredDestName   = $request->getText( 'wpDestFile' );
48                 $this->mIgnoreWarning     = $request->getCheck( 'wpIgnoreWarning' );
49                 $this->mComment           = $request->getText( 'wpUploadDescription' );
50
51                 if( !$request->wasPosted() ) {
52                         # GET requests just give the main form; no data except destination
53                         # filename and description
54                         return;
55                 }
56
57                 # Placeholders for text injection by hooks (empty per default)
58                 $this->uploadFormTextTop = "";
59                 $this->uploadFormTextAfterSummary = "";
60
61                 $this->mReUpload          = $request->getCheck( 'wpReUpload' );
62                 $this->mUploadClicked     = $request->getCheck( 'wpUpload' );
63
64                 $this->mLicense           = $request->getText( 'wpLicense' );
65                 $this->mCopyrightStatus   = $request->getText( 'wpUploadCopyStatus' );
66                 $this->mCopyrightSource   = $request->getText( 'wpUploadSource' );
67                 $this->mWatchthis         = $request->getBool( 'wpWatchthis' );
68                 $this->mSourceType        = $request->getText( 'wpSourceType' );
69                 $this->mDestWarningAck    = $request->getText( 'wpDestFileWarningAck' );
70
71                 $this->mAction            = $request->getVal( 'action' );
72
73                 $this->mSessionKey        = $request->getInt( 'wpSessionKey' );
74                 if( !empty( $this->mSessionKey ) &&
75                         isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) && 
76                         $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
77                         /**
78                          * Confirming a temporarily stashed upload.
79                          * We don't want path names to be forged, so we keep
80                          * them in the session on the server and just give
81                          * an opaque key to the user agent.
82                          */
83                         $data = $_SESSION['wsUploadData'][$this->mSessionKey];
84                         $this->mTempPath         = $data['mTempPath'];
85                         $this->mFileSize         = $data['mFileSize'];
86                         $this->mSrcName          = $data['mSrcName'];
87                         $this->mFileProps        = $data['mFileProps'];
88                         $this->mCurlError        = 0/*UPLOAD_ERR_OK*/;
89                         $this->mStashed          = true;
90                         $this->mRemoveTempFile   = false;
91                 } else {
92                         /**
93                          *Check for a newly uploaded file.
94                          */
95                         if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) {
96                                 $this->initializeFromUrl( $request );
97                         } else {
98                                 $this->initializeFromUpload( $request );
99                         }
100                 }
101         }
102
103         /**
104          * Initialize the uploaded file from PHP data
105          * @access private
106          */
107         function initializeFromUpload( $request ) {
108                 $this->mTempPath       = $request->getFileTempName( 'wpUploadFile' );
109                 $this->mFileSize       = $request->getFileSize( 'wpUploadFile' );
110                 $this->mSrcName        = $request->getFileName( 'wpUploadFile' );
111                 $this->mCurlError      = $request->getUploadError( 'wpUploadFile' );
112                 $this->mSessionKey     = false;
113                 $this->mStashed        = false;
114                 $this->mRemoveTempFile = false; // PHP will handle this
115         }
116
117         /**
118          * Copy a web file to a temporary file
119          * @access private
120          */
121         function initializeFromUrl( $request ) {
122                 global $wgTmpDirectory;
123                 $url = $request->getText( 'wpUploadFileURL' );
124                 $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
125
126                 $this->mTempPath       = $local_file;
127                 $this->mFileSize       = 0; # Will be set by curlCopy
128                 $this->mCurlError      = $this->curlCopy( $url, $local_file );
129                 $this->mSrcName        = array_pop( explode( '/', $url ) );
130                 $this->mSessionKey     = false;
131                 $this->mStashed        = false;
132
133                 // PHP won't auto-cleanup the file
134                 $this->mRemoveTempFile = file_exists( $local_file );
135         }
136
137         /**
138          * Safe copy from URL
139          * Returns true if there was an error, false otherwise
140          */
141         private function curlCopy( $url, $dest ) {
142                 global $wgUser, $wgOut;
143
144                 if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
145                         $wgOut->permissionRequired( 'upload_by_url' );
146                         return true;
147                 }
148
149                 # Maybe remove some pasting blanks :-)
150                 $url =  trim( $url );
151                 if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) {
152                         # Only HTTP or FTP URLs
153                         $wgOut->errorPage( 'upload-proto-error', 'upload-proto-error-text' );
154                         return true;
155                 }
156
157                 # Open temporary file
158                 $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
159                 if( $this->mCurlDestHandle === false ) {
160                         # Could not open temporary file to write in
161                         $wgOut->errorPage( 'upload-file-error', 'upload-file-error-text');
162                         return true;
163                 }
164
165                 $ch = curl_init();
166                 curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
167                 curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
168                 curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
169                 curl_setopt( $ch, CURLOPT_URL, $url);
170                 curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
171                 curl_exec( $ch );
172                 $error = curl_errno( $ch ) ? true : false;
173                 $errornum =  curl_errno( $ch );
174                 // if ( $error ) print curl_error ( $ch ) ; # Debugging output
175                 curl_close( $ch );
176
177                 fclose( $this->mCurlDestHandle );
178                 unset( $this->mCurlDestHandle );
179                 if( $error ) {
180                         unlink( $dest );
181                         if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) )
182                                 $wgOut->errorPage( 'upload-misc-error', 'upload-misc-error-text' );
183                         else
184                                 $wgOut->errorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" );
185                 }
186
187                 return $error;
188         }
189
190         /**
191          * Callback function for CURL-based web transfer
192          * Write data to file unless we've passed the length limit;
193          * if so, abort immediately.
194          * @access private
195          */
196         function uploadCurlCallback( $ch, $data ) {
197                 global $wgMaxUploadSize;
198                 $length = strlen( $data );
199                 $this->mFileSize += $length;
200                 if( $this->mFileSize > $wgMaxUploadSize ) {
201                         return 0;
202                 }
203                 fwrite( $this->mCurlDestHandle, $data );
204                 return $length;
205         }
206
207         /**
208          * Start doing stuff
209          * @access public
210          */
211         function execute() {
212                 global $wgUser, $wgOut;
213                 global $wgEnableUploads;
214
215                 # Check uploading enabled
216                 if( !$wgEnableUploads ) {
217                         $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
218                         return;
219                 }
220
221                 # Check permissions
222                 if( !$wgUser->isAllowed( 'upload' ) ) {
223                         if( !$wgUser->isLoggedIn() ) {
224                                 $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
225                         } else {
226                                 $wgOut->permissionRequired( 'upload' );
227                         }
228                         return;
229                 }
230
231                 # Check blocks
232                 if( $wgUser->isBlocked() ) {
233                         $wgOut->blockedPage();
234                         return;
235                 }
236
237                 if( wfReadOnly() ) {
238                         $wgOut->readOnlyPage();
239                         return;
240                 }
241
242                 if( $this->mReUpload ) {
243                         if( !$this->unsaveUploadedFile() ) {
244                                 return;
245                         }
246                         $this->mainUploadForm();
247                 } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
248                         $this->processUpload();
249                 } else {
250                         $this->mainUploadForm();
251                 }
252
253                 $this->cleanupTempFile();
254         }
255
256         /* -------------------------------------------------------------- */
257
258         /**
259          * Really do the upload
260          * Checks are made in SpecialUpload::execute()
261          * @access private
262          */
263         function processUpload() {
264                 global $wgUser, $wgOut;
265
266                 if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
267                 {
268                         wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
269                         return false;
270                 }
271
272                 /* Check for PHP error if any, requires php 4.2 or newer */
273                 if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
274                         $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
275                         return;
276                 }
277
278                 /**
279                  * If there was no filename or a zero size given, give up quick.
280                  */
281                 if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
282                         $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
283                         return;
284                 }
285
286                 # Chop off any directories in the given filename
287                 if( $this->mDesiredDestName ) {
288                         $basename = $this->mDesiredDestName;
289                 } else {
290                         $basename = $this->mSrcName;
291                 }
292                 $filtered = wfBaseName( $basename );
293
294                 /**
295                  * We'll want to blacklist against *any* 'extension', and use
296                  * only the final one for the whitelist.
297                  */
298                 list( $partname, $ext ) = $this->splitExtensions( $filtered );
299
300                 if( count( $ext ) ) {
301                         $finalExt = $ext[count( $ext ) - 1];
302                 } else {
303                         $finalExt = '';
304                 }
305
306                 # If there was more than one "extension", reassemble the base
307                 # filename to prevent bogus complaints about length
308                 if( count( $ext ) > 1 ) {
309                         for( $i = 0; $i < count( $ext ) - 1; $i++ )
310                                 $partname .= '.' . $ext[$i];
311                 }
312
313                 if( strlen( $partname ) < 1 ) {
314                         $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
315                         return;
316                 }
317
318                 /**
319                  * Filter out illegal characters, and try to make a legible name
320                  * out of it. We'll strip some silently that Title would die on.
321                  */
322                 $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered );
323                 $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
324                 if( is_null( $nt ) ) {
325                         $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
326                         return;
327                 }
328                 $this->mLocalFile = wfLocalFile( $nt );
329                 $this->mDestName = $this->mLocalFile->getName();
330
331                 /**
332                  * If the image is protected, non-sysop users won't be able
333                  * to modify it by uploading a new revision.
334                  */
335                 if( !$nt->userCan( 'edit' ) ) {
336                         return $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) );
337                 }
338
339                 /**
340                  * In some cases we may forbid overwriting of existing files.
341                  */
342                 $overwrite = $this->checkOverwrite( $this->mDestName );
343                 if( WikiError::isError( $overwrite ) ) {
344                         return $this->uploadError( $overwrite->toString() );
345                 }
346
347                 /* Don't allow users to override the blacklist (check file extension) */
348                 global $wgStrictFileExtensions;
349                 global $wgFileExtensions, $wgFileBlacklist;
350                 if ($finalExt == '') {
351                         return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
352                 } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
353                                 ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
354                         return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), 
355                                 htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) );
356                 }
357
358                 /**
359                  * Look at the contents of the file; if we can recognize the
360                  * type but it's corrupt or data of the wrong type, we should
361                  * probably not accept it.
362                  */
363                 if( !$this->mStashed ) {
364                         $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt );
365                         $this->checkMacBinary();
366                         $veri = $this->verify( $this->mTempPath, $finalExt );
367
368                         if( $veri !== true ) { //it's a wiki error...
369                                 return $this->uploadError( $veri->toString() );
370                         }
371
372                         /**
373                          * Provide an opportunity for extensions to add further checks
374                          */
375                         $error = '';
376                         if( !wfRunHooks( 'UploadVerification',
377                                         array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
378                                 return $this->uploadError( $error );
379                         }
380                 }
381
382
383                 /**
384                  * Check for non-fatal conditions
385                  */
386                 if ( ! $this->mIgnoreWarning ) {
387                         $warning = '';
388
389                         global $wgCapitalLinks;
390                         if( $wgCapitalLinks ) {
391                                 $filtered = ucfirst( $filtered );
392                         }
393                         if( $basename != $filtered ) {
394                                 $warning .=  '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>';
395                         }
396
397                         global $wgCheckFileExtensions;
398                         if ( $wgCheckFileExtensions ) {
399                                 if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
400                                         $warning .= '<li>'.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), 
401                                                 htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'</li>';
402                                 }
403                         }
404
405                         global $wgUploadSizeWarning;
406                         if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
407                                 $skin = $wgUser->getSkin();
408                                 $wsize = $skin->formatSize( $wgUploadSizeWarning );
409                                 $asize = $skin->formatSize( $this->mFileSize );
410                                 $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>';
411                         }
412                         if ( $this->mFileSize == 0 ) {
413                                 $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
414                         }
415
416                         if ( !$this->mDestWarningAck ) {
417                                 $warning .= self::getExistsWarning( $this->mLocalFile );
418                         }
419                         if( $warning != '' ) {
420                                 /**
421                                  * Stash the file in a temporary location; the user can choose
422                                  * to let it through and we'll complete the upload then.
423                                  */
424                                 return $this->uploadWarning( $warning );
425                         }
426                 }
427
428                 /**
429                  * Try actually saving the thing...
430                  * It will show an error form on failure.
431                  */
432                 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
433                         $this->mCopyrightStatus, $this->mCopyrightSource );
434
435                 $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, 
436                         File::DELETE_SOURCE, $this->mFileProps );
437                 if ( !$status->isGood() ) {
438                         $this->showError( $status->getWikiText() );
439                 } else {
440                         if ( $this->mWatchthis ) {
441                                 global $wgUser;
442                                 $wgUser->addWatch( $this->mLocalFile->getTitle() );
443                         }
444                         // Success, redirect to description page
445                         $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
446                         $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
447                         wfRunHooks( 'UploadComplete', array( &$img ) );
448                 }
449         }
450
451         /**
452          * Do existence checks on a file and produce a warning
453          * This check is static and can be done pre-upload via AJAX
454          * Returns an HTML fragment consisting of one or more LI elements if there is a warning
455          * Returns an empty string if there is no warning
456          */
457         static function getExistsWarning( $file ) {
458                 global $wgUser;
459                 // Check for uppercase extension. We allow these filenames but check if an image
460                 // with lowercase extension exists already
461                 $warning = '';
462                 
463                 if( strpos( $file->getName(), '.' ) == false ) {
464                         $partname = $file->getName();
465                         $rawExtension = '';
466                 } else {
467                         list( $partname, $rawExtension ) = explode( '.', $file->getName(), 2 );
468                 }
469                 $sk = $wgUser->getSkin();
470
471                 if ( $rawExtension != $file->getExtension() ) {
472                         // We're not using the normalized form of the extension.
473                         // Normal form is lowercase, using most common of alternate
474                         // extensions (eg 'jpg' rather than 'JPEG').
475                         //
476                         // Check for another file using the normalized form...
477                         $nt_lc = Title::newFromText( $partname . '.' . $file->getExtension() );
478                         $file_lc = wfLocalFile( $nt_lc );
479                 } else {
480                         $file_lc = false;
481                 }
482
483                 if( $file->exists() ) {
484                         $dlink = $sk->makeKnownLinkObj( $file->getTitle() );
485                         if ( $file->allowInlineDisplay() ) {
486                                 $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), 
487                                         $file->getName(), 'right', array(), false, true );
488                         } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
489                                 $icon = $file->iconThumb();
490                                 $dlink2 = '<div style="float:right" id="mw-media-icon">' . 
491                                         $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
492                         } else {
493                                 $dlink2 = '';
494                         }
495
496                         $warning .= '<li>' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '</li>' . $dlink2;
497
498                 } elseif ( $file_lc && $file_lc->exists() ) {
499                         # Check if image with lowercase extension exists.
500                         # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
501                         $dlink = $sk->makeKnownLinkObj( $nt_lc );
502                         if ( $file_lc->allowInlineDisplay() ) {
503                                 $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), 
504                                         $nt_lc->getText(), 'right', array(), false, true );
505                         } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
506                                 $icon = $file_lc->iconThumb();
507                                 $dlink2 = '<div style="float:right" id="mw-media-icon">' . 
508                                         $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
509                         } else {
510                                 $dlink2 = '';
511                         }
512
513                         $warning .= '<li>' . wfMsgExt( 'fileexists-extension', 'parsemag', $file->getName(), $dlink ) . '</li>' . $dlink2;                              
514
515                 } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) 
516                         && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) )
517                 {
518                         # Check for filenames like 50px- or 180px-, these are mostly thumbnails
519                         $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
520                         $file_thb = wfLocalFile( $nt_thb );
521                         if ($file_thb->exists() ) {
522                                 # Check if an image without leading '180px-' (or similiar) exists
523                                 $dlink = $sk->makeKnownLinkObj( $nt_thb);
524                                 if ( $file_thb->allowInlineDisplay() ) {
525                                         $dlink2 = $sk->makeImageLinkObj( $nt_thb, 
526                                                 wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), 
527                                                 $nt_thb->getText(), 'right', array(), false, true );
528                                 } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
529                                         $icon = $file_thb->iconThumb();
530                                         $dlink2 = '<div style="float:right" id="mw-media-icon">' . 
531                                                 $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . 
532                                                 $dlink . '</div>';
533                                 } else {
534                                         $dlink2 = '';
535                                 }
536
537                                 $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . 
538                                         '</li>' . $dlink2;      
539                         } else {
540                                 # Image w/o '180px-' does not exists, but we do not like these filenames
541                                 $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , 
542                                         substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
543                         }
544                 }
545                 if ( $file->wasDeleted() ) {
546                         # If the file existed before and was deleted, warn the user of this
547                         # Don't bother doing so if the image exists now, however
548                         $ltitle = SpecialPage::getTitleFor( 'Log' );
549                         $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 
550                                 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
551                         $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>';
552                 }
553                 return $warning;
554         }
555
556         static function ajaxGetExistsWarning( $filename ) {
557                 $file = wfFindFile( $filename );
558                 if( !$file ) {
559                         // Force local file so we have an object to do further checks against
560                         // if there isn't an exact match...
561                         $file = wfLocalFile( $filename );
562                 }
563                 $s = '&nbsp;';
564                 if ( $file ) {
565                         $warning = self::getExistsWarning( $file );
566                         if ( $warning !== '' ) {
567                                 $s = "<ul>$warning</ul>";
568                         }
569                 }
570                 return $s;
571         }
572         
573         /**
574          * Render a preview of a given license for the AJAX preview on upload
575          *
576          * @param string $license
577          * @return string
578          */
579         public static function ajaxGetLicensePreview( $license ) {
580                 global $wgParser, $wgUser;
581                 $text = '{{' . $license . '}}';
582                 $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
583                 $options = ParserOptions::newFromUser( $wgUser );
584                 
585                 // Expand subst: first, then live templates...
586                 $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
587                 $output = $wgParser->parse( $text, $title, $options );
588                 
589                 return $output->getText();
590         }
591
592         /**
593          * Stash a file in a temporary directory for later processing
594          * after the user has confirmed it.
595          *
596          * If the user doesn't explicitly cancel or accept, these files
597          * can accumulate in the temp directory.
598          *
599          * @param string $saveName - the destination filename
600          * @param string $tempName - the source temporary file to save
601          * @return string - full path the stashed file, or false on failure
602          * @access private
603          */
604         function saveTempUploadedFile( $saveName, $tempName ) {
605                 global $wgOut;
606                 $repo = RepoGroup::singleton()->getLocalRepo();
607                 $status = $repo->storeTemp( $saveName, $tempName );
608                 if ( !$status->isGood() ) {
609                         $this->showError( $status->getWikiText() );
610                         return false;
611                 } else {
612                         return $status->value;
613                 }
614         }
615
616         /**
617          * Stash a file in a temporary directory for later processing,
618          * and save the necessary descriptive info into the session.
619          * Returns a key value which will be passed through a form
620          * to pick up the path info on a later invocation.
621          *
622          * @return int
623          * @access private
624          */
625         function stashSession() {
626                 $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
627
628                 if( !$stash ) {
629                         # Couldn't save the file.
630                         return false;
631                 }
632
633                 $key = mt_rand( 0, 0x7fffffff );
634                 $_SESSION['wsUploadData'][$key] = array(
635                         'mTempPath'       => $stash,
636                         'mFileSize'       => $this->mFileSize,
637                         'mSrcName'        => $this->mSrcName,
638                         'mFileProps'      => $this->mFileProps,
639                         'version'         => self::SESSION_VERSION,
640                 );
641                 return $key;
642         }
643
644         /**
645          * Remove a temporarily kept file stashed by saveTempUploadedFile().
646          * @access private
647          * @return success
648          */
649         function unsaveUploadedFile() {
650                 global $wgOut;
651                 $repo = RepoGroup::singleton()->getLocalRepo();
652                 $success = $repo->freeTemp( $this->mTempPath );
653                 if ( ! $success ) {
654                         $wgOut->showFileDeleteError( $this->mTempPath );
655                         return false;
656                 } else {
657                         return true;
658                 }
659         }
660
661         /* -------------------------------------------------------------- */
662
663         /**
664          * @param string $error as HTML
665          * @access private
666          */
667         function uploadError( $error ) {
668                 global $wgOut;
669                 $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
670                 $wgOut->addHTML( "<span class='error'>{$error}</span>\n" );
671         }
672
673         /**
674          * There's something wrong with this file, not enough to reject it
675          * totally but we require manual intervention to save it for real.
676          * Stash it away, then present a form asking to confirm or cancel.
677          *
678          * @param string $warning as HTML
679          * @access private
680          */
681         function uploadWarning( $warning ) {
682                 global $wgOut, $wgContLang;
683                 global $wgUseCopyrightUpload;
684
685                 $this->mSessionKey = $this->stashSession();
686                 if( !$this->mSessionKey ) {
687                         # Couldn't save file; an error has been displayed so let's go.
688                         return;
689                 }
690
691                 $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
692                 $wgOut->addHTML( "<ul class='warning'>{$warning}</ul><br />\n" );
693
694                 $save = wfMsgHtml( 'savefile' );
695                 $reupload = wfMsgHtml( 'reupload' );
696                 $iw = wfMsgWikiHtml( 'ignorewarning' );
697                 $reup = wfMsgWikiHtml( 'reuploaddesc' );
698                 $titleObj = SpecialPage::getTitleFor( 'Upload' );
699                 $action = $titleObj->escapeLocalURL( 'action=submit' );
700                 $align1 = $wgContLang->isRTL() ? 'left' : 'right';
701                 $align2 = $wgContLang->isRTL() ? 'right' : 'left';
702
703                 if ( $wgUseCopyrightUpload )
704                 {
705                         $copyright =  "
706         <input type='hidden' name='wpUploadCopyStatus' value=\"" . htmlspecialchars( $this->mCopyrightStatus ) . "\" />
707         <input type='hidden' name='wpUploadSource' value=\"" . htmlspecialchars( $this->mCopyrightSource ) . "\" />
708         ";
709                 } else {
710                         $copyright = "";
711                 }
712
713                 $wgOut->addHTML( "
714         <form id='uploadwarning' method='post' enctype='multipart/form-data' action='$action'>
715                 <input type='hidden' name='wpIgnoreWarning' value='1' />
716                 <input type='hidden' name='wpSessionKey' value=\"" . htmlspecialchars( $this->mSessionKey ) . "\" />
717                 <input type='hidden' name='wpUploadDescription' value=\"" . htmlspecialchars( $this->mComment ) . "\" />
718                 <input type='hidden' name='wpLicense' value=\"" . htmlspecialchars( $this->mLicense ) . "\" />
719                 <input type='hidden' name='wpDestFile' value=\"" . htmlspecialchars( $this->mDesiredDestName ) . "\" />
720                 <input type='hidden' name='wpWatchthis' value=\"" . htmlspecialchars( intval( $this->mWatchthis ) ) . "\" />
721         {$copyright}
722         <table border='0'>
723                 <tr>
724                         <tr>
725                                 <td align='$align1'>
726                                         <input tabindex='2' type='submit' name='wpUpload' value=\"$save\" />
727                                 </td>
728                                 <td align='$align2'>$iw</td>
729                         </tr>
730                         <tr>
731                                 <td align='$align1'>
732                                         <input tabindex='2' type='submit' name='wpReUpload' value=\"{$reupload}\" />
733                                 </td>
734                                 <td align='$align2'>$reup</td>
735                         </tr>
736                 </tr>
737         </table></form>\n" );
738         }
739
740         /**
741          * Displays the main upload form, optionally with a highlighted
742          * error message up at the top.
743          *
744          * @param string $msg as HTML
745          * @access private
746          */
747         function mainUploadForm( $msg='' ) {
748                 global $wgOut, $wgUser, $wgContLang;
749                 global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview;
750                 global $wgRequest, $wgAllowCopyUploads;
751                 global $wgStylePath, $wgStyleVersion;
752
753                 $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
754                 $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
755                 
756                 $adc = wfBoolToStr( $useAjaxDestCheck );
757                 $alp = wfBoolToStr( $useAjaxLicensePreview );
758                 
759                 $wgOut->addScript( "<script type=\"text/javascript\">
760 wgAjaxUploadDestCheck = {$adc};
761 wgAjaxLicensePreview = {$alp};
762 </script>
763 <script type=\"text/javascript\" src=\"{$wgStylePath}/common/upload.js?{$wgStyleVersion}\"></script>
764                 " );
765
766                 if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
767                 {
768                         wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
769                         return false;
770                 }
771                 
772                 if( $this->mDesiredDestName && $wgUser->isAllowed( 'deletedhistory' ) ) {
773                         $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
774                         if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 ) {
775                                 $link = wfMsgExt(
776                                         $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
777                                         array( 'parse', 'replaceafter' ),
778                                         $wgUser->getSkin()->makeKnownLinkObj(
779                                                 SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
780                                                 wfMsgHtml( 'restorelink', $count )
781                                         )
782                                 );
783                                 $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
784                         }                               
785                 }
786
787                 $cols = intval($wgUser->getOption( 'cols' ));
788                 $ew = $wgUser->getOption( 'editwidth' );
789                 if ( $ew ) $ew = " style=\"width:100%\"";
790                 else $ew = '';
791
792                 if ( '' != $msg ) {
793                         $sub = wfMsgHtml( 'uploaderror' );
794                         $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
795                           "<span class='error'>{$msg}</span>\n" );
796                 }
797                 $wgOut->addHTML( '<div id="uploadtext">' );
798                 $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) );
799                 $wgOut->addHTML( '</div>' );
800
801                 $sourcefilename = wfMsgHtml( 'sourcefilename' );
802                 $destfilename = wfMsgHtml( 'destfilename' );
803                 $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' );
804
805                 $licenses = new Licenses();
806                 $license = wfMsgExt( 'license', array( 'parseinline' ) );
807                 $nolicense = wfMsgHtml( 'nolicense' );
808                 $licenseshtml = $licenses->getHtml();
809
810                 $ulb = wfMsgHtml( 'uploadbtn' );
811
812
813                 $titleObj = SpecialPage::getTitleFor( 'Upload' );
814                 $action = $titleObj->escapeLocalURL();
815
816                 $encDestName = htmlspecialchars( $this->mDesiredDestName );
817
818                 $watchChecked =
819                         ( $wgUser->getOption( 'watchdefault' ) ||
820                                 ( $wgUser->getOption( 'watchcreations' ) && $this->mDesiredDestName == '' ) )
821                         ? 'checked="checked"'
822                         : '';
823                 $warningChecked = $this->mIgnoreWarning ? 'checked' : '';
824
825                 // Prepare form for upload or upload/copy
826                 if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) {
827                         $filename_form =
828                                 "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' " .
829                                    "onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked />" .
830                                  "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
831                                    "onfocus='" . 
832                                      "toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");" .
833                                      "toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")'" .
834                                 ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" .
835                                 wfMsgHTML( 'upload_source_file' ) . "<br/>" .
836                                 "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' " .
837                                   "onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" .
838                                 "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' " .
839                                   "onfocus='" .
840                                     "toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");" .
841                                     "toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")'" .
842                                 ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" .
843                                 wfMsgHtml( 'upload_source_url' ) ;
844                 } else {
845                         $filename_form =
846                                 "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
847                                 ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") .
848                                 "size='40' />" .
849                                 "<input type='hidden' name='wpSourceType' value='file' />" ;
850                 }
851                 if ( $useAjaxDestCheck ) {
852                         $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'>&nbsp;</td></tr>";
853                         $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
854                 } else {
855                         $warningRow = '';
856                         $destOnkeyup = '';
857                 }
858
859                 $encComment = htmlspecialchars( $this->mComment );
860                 $align1 = $wgContLang->isRTL() ? 'left' : 'right';
861                 $align2 = $wgContLang->isRTL() ? 'right' : 'left';
862
863                 $wgOut->addHTML( <<<EOT
864         <form id='upload' method='post' enctype='multipart/form-data' action="$action">
865                 <table border='0'>
866                 <tr>
867           {$this->uploadFormTextTop}
868                         <td align='$align1' valign='top'><label for='wpUploadFile'>{$sourcefilename}:</label></td>
869                         <td align='$align2'>
870                                 {$filename_form}
871                         </td>
872                 </tr>
873                 <tr>
874                         <td align='$align1'><label for='wpDestFile'>{$destfilename}:</label></td>
875                         <td align='$align2'>
876                                 <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='40' 
877                                         value="$encDestName" $destOnkeyup />
878                         </td>
879                 </tr>
880                 <tr>
881                         <td align='$align1'><label for='wpUploadDescription'>{$summary}</label></td>
882                         <td align='$align2'>
883                                 <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' 
884                                         cols='{$cols}'{$ew}>$encComment</textarea>
885            {$this->uploadFormTextAfterSummary}
886                         </td>
887                 </tr>
888                 <tr>
889 EOT
890                 );
891
892                 if ( $licenseshtml != '' ) {
893                         global $wgStylePath;
894                         $wgOut->addHTML( "
895                         <td align='$align1'><label for='wpLicense'>$license:</label></td>
896                         <td align='$align2'>
897                                 <select name='wpLicense' id='wpLicense' tabindex='4'
898                                         onchange='licenseSelectorCheck()'>
899                                         <option value=''>$nolicense</option>
900                                         $licenseshtml
901                                 </select>
902                         </td>
903                         </tr>
904                         <tr>" );
905                         if( $useAjaxLicensePreview ) {
906                                 $wgOut->addHtml( "
907                                         <td></td>
908                                         <td id=\"mw-license-preview\"></td>
909                                 </tr>
910                                 <tr>" );
911                         }
912                 }
913
914                 if ( $wgUseCopyrightUpload ) {
915                         $filestatus = wfMsgHtml ( 'filestatus' );
916                         $copystatus =  htmlspecialchars( $this->mCopyrightStatus );
917                         $filesource = wfMsgHtml ( 'filesource' );
918                         $uploadsource = htmlspecialchars( $this->mCopyrightSource );
919
920                         $wgOut->addHTML( "
921                                 <td align='$align1' nowrap='nowrap'><label for='wpUploadCopyStatus'>$filestatus:</label></td>
922                                         <td><input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus' 
923                                           value=\"$copystatus\" size='40' /></td>
924                         </tr>
925                         <tr>
926                                 <td align='$align1'><label for='wpUploadCopyStatus'>$filesource:</label></td>
927                                         <td><input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus' 
928                                           value=\"$uploadsource\" size='40' /></td>
929                         </tr>
930                         <tr>
931                 ");
932                 }
933
934                 $wgOut->addHtml( "
935                 <td></td>
936                 <td>
937                         <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
938                         <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label>
939                         <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked/>
940                         <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
941                 </td>
942         </tr>
943         $warningRow
944         <tr>
945                 <td></td>
946                 <td align='$align2'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " /></td>
947         </tr>
948         <tr>
949                 <td></td>
950                 <td align='$align2'>
951                 " );
952                 $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
953                 $wgOut->addHTML( "
954                 </td>
955         </tr>
956
957         </table>
958         <input type='hidden' name='wpDestFileWarningAck' id='wpDestFileWarningAck' value=''/>
959         </form>" );
960         }
961
962         /* -------------------------------------------------------------- */
963
964         /**
965          * Split a file into a base name and all dot-delimited 'extensions'
966          * on the end. Some web server configurations will fall back to
967          * earlier pseudo-'extensions' to determine type and execute
968          * scripts, so the blacklist needs to check them all.
969          *
970          * @return array
971          */
972         function splitExtensions( $filename ) {
973                 $bits = explode( '.', $filename );
974                 $basename = array_shift( $bits );
975                 return array( $basename, $bits );
976         }
977
978         /**
979          * Perform case-insensitive match against a list of file extensions.
980          * Returns true if the extension is in the list.
981          *
982          * @param string $ext
983          * @param array $list
984          * @return bool
985          */
986         function checkFileExtension( $ext, $list ) {
987                 return in_array( strtolower( $ext ), $list );
988         }
989
990         /**
991          * Perform case-insensitive match against a list of file extensions.
992          * Returns true if any of the extensions are in the list.
993          *
994          * @param array $ext
995          * @param array $list
996          * @return bool
997          */
998         function checkFileExtensionList( $ext, $list ) {
999                 foreach( $ext as $e ) {
1000                         if( in_array( strtolower( $e ), $list ) ) {
1001                                 return true;
1002                         }
1003                 }
1004                 return false;
1005         }
1006
1007         /**
1008          * Verifies that it's ok to include the uploaded file
1009          *
1010          * @param string $tmpfile the full path of the temporary file to verify
1011          * @param string $extension The filename extension that the file is to be served with
1012          * @return mixed true of the file is verified, a WikiError object otherwise.
1013          */
1014         function verify( $tmpfile, $extension ) {
1015                 #magically determine mime type
1016                 $magic=& MimeMagic::singleton();
1017                 $mime= $magic->guessMimeType($tmpfile,false);
1018
1019                 #check mime type, if desired
1020                 global $wgVerifyMimeType;
1021                 if ($wgVerifyMimeType) {
1022
1023                   wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
1024                         #check mime type against file extension
1025                         if( !$this->verifyExtension( $mime, $extension ) ) {
1026                                 return new WikiErrorMsg( 'uploadcorrupt' );
1027                         }
1028
1029                         #check mime type blacklist
1030                         global $wgMimeTypeBlacklist;
1031                         if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
1032                                 && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
1033                                 return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
1034                         }
1035                 }
1036
1037                 #check for htmlish code and javascript
1038                 if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
1039                         return new WikiErrorMsg( 'uploadscripted' );
1040                 }
1041
1042                 /**
1043                 * Scan the uploaded file for viruses
1044                 */
1045                 $virus= $this->detectVirus($tmpfile);
1046                 if ( $virus ) {
1047                         return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
1048                 }
1049
1050                 wfDebug( __METHOD__.": all clear; passing.\n" );
1051                 return true;
1052         }
1053
1054         /**
1055          * Checks if the mime type of the uploaded file matches the file extension.
1056          *
1057          * @param string $mime the mime type of the uploaded file
1058          * @param string $extension The filename extension that the file is to be served with
1059          * @return bool
1060          */
1061         function verifyExtension( $mime, $extension ) {
1062                 $magic =& MimeMagic::singleton();
1063
1064                 if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
1065                         if ( ! $magic->isRecognizableExtension( $extension ) ) {
1066                                 wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
1067                                         "unrecognized extension '$extension', can't verify\n" );
1068                                 return true;
1069                         } else {
1070                                 wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
1071                                         "recognized extension '$extension', so probably invalid file\n" );
1072                                 return false;
1073                         }
1074
1075                 $match= $magic->isMatchingExtension($extension,$mime);
1076
1077                 if ($match===NULL) {
1078                         wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
1079                         return true;
1080                 } elseif ($match===true) {
1081                         wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
1082
1083                         #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
1084                         return true;
1085
1086                 } else {
1087                         wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
1088                         return false;
1089                 }
1090         }
1091
1092         /** 
1093          * Heuristic for detecting files that *could* contain JavaScript instructions or
1094          * things that may look like HTML to a browser and are thus
1095          * potentially harmful. The present implementation will produce false positives in some situations.
1096          *
1097          * @param string $file Pathname to the temporary upload file
1098          * @param string $mime The mime type of the file
1099          * @param string $extension The extension of the file
1100          * @return bool true if the file contains something looking like embedded scripts
1101          */
1102         function detectScript($file, $mime, $extension) {
1103                 global $wgAllowTitlesInSVG;
1104
1105                 #ugly hack: for text files, always look at the entire file.
1106                 #For binarie field, just check the first K.
1107
1108                 if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
1109                 else {
1110                         $fp = fopen( $file, 'rb' );
1111                         $chunk = fread( $fp, 1024 );
1112                         fclose( $fp );
1113                 }
1114
1115                 $chunk= strtolower( $chunk );
1116
1117                 if (!$chunk) return false;
1118
1119                 #decode from UTF-16 if needed (could be used for obfuscation).
1120                 if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
1121                 elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
1122                 else $enc= NULL;
1123
1124                 if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
1125
1126                 $chunk= trim($chunk);
1127
1128                 #FIXME: convert from UTF-16 if necessarry!
1129
1130                 wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
1131
1132                 #check for HTML doctype
1133                 if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
1134
1135                 /**
1136                 * Internet Explorer for Windows performs some really stupid file type
1137                 * autodetection which can cause it to interpret valid image files as HTML
1138                 * and potentially execute JavaScript, creating a cross-site scripting
1139                 * attack vectors.
1140                 *
1141                 * Apple's Safari browser also performs some unsafe file type autodetection
1142                 * which can cause legitimate files to be interpreted as HTML if the
1143                 * web server is not correctly configured to send the right content-type
1144                 * (or if you're really uploading plain text and octet streams!)
1145                 *
1146                 * Returns true if IE is likely to mistake the given file for HTML.
1147                 * Also returns true if Safari would mistake the given file for HTML
1148                 * when served with a generic content-type.
1149                 */
1150
1151                 $tags = array(
1152                         '<body',
1153                         '<head',
1154                         '<html',   #also in safari
1155                         '<img',
1156                         '<pre',
1157                         '<script', #also in safari
1158                         '<table'
1159                         );
1160                 if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
1161                         $tags[] = '<title';
1162                 }
1163
1164                 foreach( $tags as $tag ) {
1165                         if( false !== strpos( $chunk, $tag ) ) {
1166                                 return true;
1167                         }
1168                 }
1169
1170                 /*
1171                 * look for javascript
1172                 */
1173
1174                 #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1175                 $chunk = Sanitizer::decodeCharReferences( $chunk );
1176
1177                 #look for script-types
1178                 if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true;
1179
1180                 #look for html-style script-urls
1181                 if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
1182
1183                 #look for css-style script-urls
1184                 if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
1185
1186                 wfDebug("SpecialUpload::detectScript: no scripts found\n");
1187                 return false;
1188         }
1189
1190         /** 
1191          * Generic wrapper function for a virus scanner program.
1192          * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
1193          * $wgAntivirusRequired may be used to deny upload if the scan fails.
1194          *
1195          * @param string $file Pathname to the temporary upload file
1196          * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
1197          *         or a string containing feedback from the virus scanner if a virus was found.
1198          *         If textual feedback is missing but a virus was found, this function returns true.
1199          */
1200         function detectVirus($file) {
1201                 global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
1202
1203                 if ( !$wgAntivirus ) {
1204                         wfDebug( __METHOD__.": virus scanner disabled\n");
1205                         return NULL;
1206                 }
1207
1208                 if ( !$wgAntivirusSetup[$wgAntivirus] ) {
1209                         wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
1210                         # @TODO: localise
1211                         $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" ); 
1212                         return "unknown antivirus: $wgAntivirus";
1213                 }
1214
1215                 # look up scanner configuration
1216                 $command = $wgAntivirusSetup[$wgAntivirus]["command"];
1217                 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
1218                 $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
1219                         $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
1220
1221                 if ( strpos( $command,"%f" ) === false ) {
1222                         # simple pattern: append file to scan
1223                         $command .= " " . wfEscapeShellArg( $file ); 
1224                 } else {
1225                         # complex pattern: replace "%f" with file to scan
1226                         $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); 
1227                 }
1228
1229                 wfDebug( __METHOD__.": running virus scan: $command \n" );
1230
1231                 # execute virus scanner
1232                 $exitCode = false;
1233
1234                 #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1235                 #      that does not seem to be worth the pain.
1236                 #      Ask me (Duesentrieb) about it if it's ever needed.
1237                 $output = array();
1238                 if ( wfIsWindows() ) {
1239                         exec( "$command", $output, $exitCode );
1240                 } else {
1241                         exec( "$command 2>&1", $output, $exitCode );
1242                 }
1243
1244                 # map exit code to AV_xxx constants.
1245                 $mappedCode = $exitCode;
1246                 if ( $exitCodeMap ) { 
1247                         if ( isset( $exitCodeMap[$exitCode] ) ) {
1248                                 $mappedCode = $exitCodeMap[$exitCode];
1249                         } elseif ( isset( $exitCodeMap["*"] ) ) {
1250                                 $mappedCode = $exitCodeMap["*"];
1251                         }
1252                 }
1253
1254                 if ( $mappedCode === AV_SCAN_FAILED ) { 
1255                         # scan failed (code was mapped to false by $exitCodeMap)
1256                         wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
1257
1258                         if ( $wgAntivirusRequired ) { 
1259                                 return "scan failed (code $exitCode)"; 
1260                         } else { 
1261                                 return NULL; 
1262                         }
1263                 } else if ( $mappedCode === AV_SCAN_ABORTED ) { 
1264                         # scan failed because filetype is unknown (probably imune)
1265                         wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
1266                         return NULL;
1267                 } else if ( $mappedCode === AV_NO_VIRUS ) {
1268                         # no virus found
1269                         wfDebug( __METHOD__.": file passed virus scan.\n" );
1270                         return false;
1271                 } else {
1272                         $output = join( "\n", $output );
1273                         $output = trim( $output );
1274
1275                         if ( !$output ) {
1276                                 $output = true; #if there's no output, return true
1277                         } elseif ( $msgPattern ) {
1278                                 $groups = array();
1279                                 if ( preg_match( $msgPattern, $output, $groups ) ) {
1280                                         if ( $groups[1] ) {
1281                                                 $output = $groups[1];
1282                                         }
1283                                 }
1284                         }
1285
1286                         wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
1287                         return $output;
1288                 }
1289         }
1290
1291         /**
1292          * Check if the temporary file is MacBinary-encoded, as some uploads
1293          * from Internet Explorer on Mac OS Classic and Mac OS X will be.
1294          * If so, the data fork will be extracted to a second temporary file,
1295          * which will then be checked for validity and either kept or discarded.
1296          *
1297          * @access private
1298          */
1299         function checkMacBinary() {
1300                 $macbin = new MacBinary( $this->mTempPath );
1301                 if( $macbin->isValid() ) {
1302                         $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
1303                         $dataHandle = fopen( $dataFile, 'wb' );
1304
1305                         wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
1306                         $macbin->extractData( $dataHandle );
1307
1308                         $this->mTempPath = $dataFile;
1309                         $this->mFileSize = $macbin->dataForkLength();
1310
1311                         // We'll have to manually remove the new file if it's not kept.
1312                         $this->mRemoveTempFile = true;
1313                 }
1314                 $macbin->close();
1315         }
1316
1317         /**
1318          * If we've modified the upload file we need to manually remove it
1319          * on exit to clean up.
1320          * @access private
1321          */
1322         function cleanupTempFile() {
1323                 if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
1324                         wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
1325                         unlink( $this->mTempPath );
1326                 }
1327         }
1328
1329         /**
1330          * Check if there's an overwrite conflict and, if so, if restrictions
1331          * forbid this user from performing the upload.
1332          *
1333          * @return mixed true on success, WikiError on failure
1334          * @access private
1335          */
1336         function checkOverwrite( $name ) {
1337                 $img = wfFindFile( $name );
1338
1339                 $error = '';
1340                 if( $img ) {
1341                         global $wgUser, $wgOut;
1342                         if( $img->isLocal() ) {
1343                                 if( !self::userCanReUpload( $wgUser, $img->name ) ) {
1344                                         $error = 'fileexists-forbidden';
1345                                 }
1346                         } else {
1347                                 if( !$wgUser->isAllowed( 'reupload' ) ||
1348                                     !$wgUser->isAllowed( 'reupload-shared' ) ) {
1349                                         $error = "fileexists-shared-forbidden";
1350                                 }
1351                         }
1352                 }
1353
1354                 if( $error ) {
1355                         $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
1356                         return new WikiError( $wgOut->parse( $errorText ) );
1357                 }
1358
1359                 // Rockin', go ahead and upload
1360                 return true;
1361         }
1362
1363          /**
1364          * Check if a user is the last uploader
1365          *
1366          * @param User $user
1367          * @param string $img, image name
1368          * @return bool
1369          */
1370         public static function userCanReUpload( User $user, $img ) {
1371                 if( $user->isAllowed( 'reupload' ) )
1372                         return true; // non-conditional
1373                 if( !$user->isAllowed( 'reupload-own' ) )
1374                         return false;
1375                 
1376                 $dbr = wfGetDB( DB_SLAVE );
1377                 $row = $dbr->selectRow('image',
1378                 /* SELECT */ 'img_user',
1379                 /* WHERE */ array( 'img_name' => $img )
1380                 );
1381                 if ( !$row )
1382                         return false;
1383
1384                 return $user->getID() == $row->img_user;
1385         }
1386
1387         /**
1388          * Display an error with a wikitext description
1389          */
1390         function showError( $description ) {
1391                 global $wgOut;
1392                 $wgOut->setPageTitle( wfMsg( "internalerror" ) );
1393                 $wgOut->setRobotpolicy( "noindex,nofollow" );
1394                 $wgOut->setArticleRelated( false );
1395                 $wgOut->enableClientCache( false );
1396                 $wgOut->addWikiText( $description );
1397         }
1398
1399         /**
1400          * Get the initial image page text based on a comment and optional file status information
1401          */
1402         static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
1403                 global $wgUseCopyrightUpload;
1404                 if ( $wgUseCopyrightUpload ) {
1405                         if ( $license != '' ) {
1406                                 $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
1407                         }
1408                         $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
1409                           '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
1410                           "$licensetxt" .
1411                           '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
1412                 } else {
1413                         if ( $license != '' ) {
1414                                 $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
1415                                  $pageText = $filedesc .
1416                                          '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
1417                         } else {
1418                                 $pageText = $comment;
1419                         }
1420                 }
1421                 return $pageText;
1422         }
1423 }