nuke UpdateClasses.php, autoload MacBinary, Licenses
[lhc/web/wiklou.git] / includes / SpecialUpload.php
1 <?php
2 /**
3 *
4 * @package MediaWiki
5 * @subpackage SpecialPage
6 */
7
8 /**
9 *
10 */
11 require_once 'Image.php';
12 /**
13 * Entry point
14 */
15 function wfSpecialUpload() {
16 global $wgRequest;
17 $form = new UploadForm( $wgRequest );
18 $form->execute();
19 }
20
21 /**
22 *
23 * @package MediaWiki
24 * @subpackage SpecialPage
25 */
26 class UploadForm {
27 /**#@+
28 * @access private
29 */
30 var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError;
31 var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion;
32 var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload;
33 var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile;
34 /**#@-*/
35
36 /**
37 * Constructor : initialise object
38 * Get data POSTed through the form and assign them to the object
39 * @param $request Data posted.
40 */
41 function UploadForm( &$request ) {
42 $this->mDestFile = $request->getText( 'wpDestFile' );
43
44 if( !$request->wasPosted() ) {
45 # GET requests just give the main form; no data except wpDestfile.
46 return;
47 }
48
49 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
50 $this->mReUpload = $request->getCheck( 'wpReUpload' );
51 $this->mUpload = $request->getCheck( 'wpUpload' );
52
53 $this->mUploadDescription = $request->getText( 'wpUploadDescription' );
54 $this->mLicense = $request->getText( 'wpLicense' );
55 $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' );
56 $this->mUploadSource = $request->getText( 'wpUploadSource' );
57 $this->mWatchthis = $request->getBool( 'wpWatchthis' );
58 wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" );
59
60 $this->mAction = $request->getVal( 'action' );
61
62 $this->mSessionKey = $request->getInt( 'wpSessionKey' );
63 if( !empty( $this->mSessionKey ) &&
64 isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) {
65 /**
66 * Confirming a temporarily stashed upload.
67 * We don't want path names to be forged, so we keep
68 * them in the session on the server and just give
69 * an opaque key to the user agent.
70 */
71 $data = $_SESSION['wsUploadData'][$this->mSessionKey];
72 $this->mUploadTempName = $data['mUploadTempName'];
73 $this->mUploadSize = $data['mUploadSize'];
74 $this->mOname = $data['mOname'];
75 $this->mUploadError = 0/*UPLOAD_ERR_OK*/;
76 $this->mStashed = true;
77 $this->mRemoveTempFile = false;
78 } else {
79 /**
80 *Check for a newly uploaded file.
81 */
82 $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' );
83 $this->mUploadSize = $request->getFileSize( 'wpUploadFile' );
84 $this->mOname = $request->getFileName( 'wpUploadFile' );
85 $this->mUploadError = $request->getUploadError( 'wpUploadFile' );
86 $this->mSessionKey = false;
87 $this->mStashed = false;
88 $this->mRemoveTempFile = false; // PHP will handle this
89 }
90 }
91
92 /**
93 * Start doing stuff
94 * @access public
95 */
96 function execute() {
97 global $wgUser, $wgOut;
98 global $wgEnableUploads, $wgUploadDirectory;
99
100 # Check uploading enabled
101 if( !$wgEnableUploads ) {
102 $wgOut->errorPage( 'uploaddisabled', 'uploaddisabledtext' );
103 return;
104 }
105
106 # Check permissions
107 if( $wgUser->isLoggedIn() ) {
108 if( !$wgUser->isAllowed( 'upload' ) ) {
109 $wgOut->permissionRequired( 'upload' );
110 return;
111 }
112 } else {
113 $wgOut->errorPage( 'uploadnologin', 'uploadnologintext' );
114 return;
115 }
116
117 # Check blocks
118 if( $wgUser->isBlocked() ) {
119 $wgOut->blockedPage();
120 return;
121 }
122
123 if( wfReadOnly() ) {
124 $wgOut->readOnlyPage();
125 return;
126 }
127
128 /** Check if the image directory is writeable, this is a common mistake */
129 if ( !is_writeable( $wgUploadDirectory ) ) {
130 $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) );
131 return;
132 }
133
134 if( $this->mReUpload ) {
135 $this->unsaveUploadedFile();
136 $this->mainUploadForm();
137 } else if ( 'submit' == $this->mAction || $this->mUpload ) {
138 $this->processUpload();
139 } else {
140 $this->mainUploadForm();
141 }
142
143 $this->cleanupTempFile();
144 }
145
146 /* -------------------------------------------------------------- */
147
148 /**
149 * Really do the upload
150 * Checks are made in SpecialUpload::execute()
151 * @access private
152 */
153 function processUpload() {
154 global $wgUser, $wgOut;
155
156 /* Check for PHP error if any, requires php 4.2 or newer */
157 if ( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
158 $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
159 return;
160 }
161
162 /**
163 * If there was no filename or a zero size given, give up quick.
164 */
165 if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) {
166 $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
167 return;
168 }
169
170 # Chop off any directories in the given filename
171 if ( $this->mDestFile ) {
172 $basename = wfBaseName( $this->mDestFile );
173 } else {
174 $basename = wfBaseName( $this->mOname );
175 }
176
177 /**
178 * We'll want to blacklist against *any* 'extension', and use
179 * only the final one for the whitelist.
180 */
181 list( $partname, $ext ) = $this->splitExtensions( $basename );
182
183 if( count( $ext ) ) {
184 $finalExt = $ext[count( $ext ) - 1];
185 } else {
186 $finalExt = '';
187 }
188 $fullExt = implode( '.', $ext );
189
190 # If there was more than one "extension", reassemble the base
191 # filename to prevent bogus complaints about length
192 if( count( $ext ) > 1 ) {
193 for( $i = 0; $i < count( $ext ) - 1; $i++ )
194 $partname .= '.' . $ext[$i];
195 }
196
197 if ( strlen( $partname ) < 3 ) {
198 $this->mainUploadForm( wfMsgHtml( 'minlength' ) );
199 return;
200 }
201
202 /**
203 * Filter out illegal characters, and try to make a legible name
204 * out of it. We'll strip some silently that Title would die on.
205 */
206 $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename );
207 $nt = Title::newFromText( $filtered );
208 if( is_null( $nt ) ) {
209 $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
210 return;
211 }
212 $nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() );
213 $this->mUploadSaveName = $nt->getDBkey();
214
215 /**
216 * If the image is protected, non-sysop users won't be able
217 * to modify it by uploading a new revision.
218 */
219 if( !$nt->userCanEdit() ) {
220 return $this->uploadError( wfMsgWikiHtml( 'protectedpage' ) );
221 }
222
223 /**
224 * In some cases we may forbid overwriting of existing files.
225 */
226 $overwrite = $this->checkOverwrite( $this->mUploadSaveName );
227 if( WikiError::isError( $overwrite ) ) {
228 return $this->uploadError( $overwrite->toString() );
229 }
230
231 /* Don't allow users to override the blacklist (check file extension) */
232 global $wgStrictFileExtensions;
233 global $wgFileExtensions, $wgFileBlacklist;
234 if( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
235 ($wgStrictFileExtensions &&
236 !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
237 return $this->uploadError( wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ) );
238 }
239
240 /**
241 * Look at the contents of the file; if we can recognize the
242 * type but it's corrupt or data of the wrong type, we should
243 * probably not accept it.
244 */
245 if( !$this->mStashed ) {
246 $this->checkMacBinary();
247 $veri = $this->verify( $this->mUploadTempName, $finalExt );
248
249 if( $veri !== true ) { //it's a wiki error...
250 return $this->uploadError( $veri->toString() );
251 }
252 }
253
254 /**
255 * Provide an opportunity for extensions to add futher checks
256 */
257 $error = '';
258 if( !wfRunHooks( 'UploadVerification',
259 array( $this->mUploadSaveName, $this->mUploadTempName, &$error ) ) ) {
260 return $this->uploadError( $error );
261 }
262
263 /**
264 * Check for non-fatal conditions
265 */
266 if ( ! $this->mIgnoreWarning ) {
267 $warning = '';
268
269 global $wgCapitalLinks;
270 if( $wgCapitalLinks ) {
271 $filtered = ucfirst( $filtered );
272 }
273 if( $this->mUploadSaveName != $filtered ) {
274 $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mUploadSaveName ) ).'</li>';
275 }
276
277 global $wgCheckFileExtensions;
278 if ( $wgCheckFileExtensions ) {
279 if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
280 $warning .= '<li>'.wfMsgHtml( 'badfiletype', htmlspecialchars( $fullExt ) ).'</li>';
281 }
282 }
283
284 global $wgUploadSizeWarning;
285 if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) {
286 # TODO: Format $wgUploadSizeWarning to something that looks better than the raw byte
287 # value, perhaps add GB,MB and KB suffixes?
288 $warning .= '<li>'.wfMsgHtml( 'largefile', $wgUploadSizeWarning, $this->mUploadSize ).'</li>';
289 }
290 if ( $this->mUploadSize == 0 ) {
291 $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
292 }
293
294 if( $nt->getArticleID() ) {
295 global $wgUser;
296 $sk = $wgUser->getSkin();
297 $dlink = $sk->makeKnownLinkObj( $nt );
298 $warning .= '<li>'.wfMsgHtml( 'fileexists', $dlink ).'</li>';
299 } else {
300 # If the file existed before and was deleted, warn the user of this
301 # Don't bother doing so if the image exists now, however
302 $image = new Image( $nt );
303 if( $image->wasDeleted() ) {
304 $skin = $wgUser->getSkin();
305 $ltitle = Title::makeTitle( NS_SPECIAL, 'Log' );
306 $llink = $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() );
307 $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' );
308 }
309 }
310
311 if( $warning != '' ) {
312 /**
313 * Stash the file in a temporary location; the user can choose
314 * to let it through and we'll complete the upload then.
315 */
316 return $this->uploadWarning( $warning );
317 }
318 }
319
320 /**
321 * Try actually saving the thing...
322 * It will show an error form on failure.
323 */
324 $hasBeenMunged = !empty( $this->mSessionKey ) || $this->mRemoveTempFile;
325 if( $this->saveUploadedFile( $this->mUploadSaveName,
326 $this->mUploadTempName,
327 $hasBeenMunged ) ) {
328 /**
329 * Update the upload log and create the description page
330 * if it's a new file.
331 */
332 $img = Image::newFromName( $this->mUploadSaveName );
333 $success = $img->recordUpload( $this->mUploadOldVersion,
334 $this->mUploadDescription,
335 $this->mLicense,
336 $this->mUploadCopyStatus,
337 $this->mUploadSource,
338 $this->mWatchthis );
339
340 if ( $success ) {
341 $this->showSuccess();
342 wfRunHooks( 'UploadComplete', array( &$img ) );
343 } else {
344 // Image::recordUpload() fails if the image went missing, which is
345 // unlikely, hence the lack of a specialised message
346 $wgOut->fileNotFoundError( $this->mUploadSaveName );
347 }
348 }
349 }
350
351 /**
352 * Move the uploaded file from its temporary location to the final
353 * destination. If a previous version of the file exists, move
354 * it into the archive subdirectory.
355 *
356 * @todo If the later save fails, we may have disappeared the original file.
357 *
358 * @param string $saveName
359 * @param string $tempName full path to the temporary file
360 * @param bool $useRename if true, doesn't check that the source file
361 * is a PHP-managed upload temporary
362 */
363 function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
364 global $wgOut;
365
366 $fname= "SpecialUpload::saveUploadedFile";
367
368 $dest = wfImageDir( $saveName );
369 $archive = wfImageArchiveDir( $saveName );
370 $this->mSavedFile = "{$dest}/{$saveName}";
371
372 if( is_file( $this->mSavedFile ) ) {
373 $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
374 wfSuppressWarnings();
375 $success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" );
376 wfRestoreWarnings();
377
378 if( ! $success ) {
379 $wgOut->fileRenameError( $this->mSavedFile,
380 "${archive}/{$this->mUploadOldVersion}" );
381 return false;
382 }
383 else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n");
384 }
385 else {
386 $this->mUploadOldVersion = '';
387 }
388
389 wfSuppressWarnings();
390 $success = $useRename
391 ? rename( $tempName, $this->mSavedFile )
392 : move_uploaded_file( $tempName, $this->mSavedFile );
393 wfRestoreWarnings();
394
395 if( ! $success ) {
396 $wgOut->fileCopyError( $tempName, $this->mSavedFile );
397 return false;
398 } else {
399 wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n");
400 }
401
402 chmod( $this->mSavedFile, 0644 );
403 return true;
404 }
405
406 /**
407 * Stash a file in a temporary directory for later processing
408 * after the user has confirmed it.
409 *
410 * If the user doesn't explicitly cancel or accept, these files
411 * can accumulate in the temp directory.
412 *
413 * @param string $saveName - the destination filename
414 * @param string $tempName - the source temporary file to save
415 * @return string - full path the stashed file, or false on failure
416 * @access private
417 */
418 function saveTempUploadedFile( $saveName, $tempName ) {
419 global $wgOut;
420 $archive = wfImageArchiveDir( $saveName, 'temp' );
421 $stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName;
422
423 $success = $this->mRemoveTempFile
424 ? rename( $tempName, $stash )
425 : move_uploaded_file( $tempName, $stash );
426 if ( !$success ) {
427 $wgOut->fileCopyError( $tempName, $stash );
428 return false;
429 }
430
431 return $stash;
432 }
433
434 /**
435 * Stash a file in a temporary directory for later processing,
436 * and save the necessary descriptive info into the session.
437 * Returns a key value which will be passed through a form
438 * to pick up the path info on a later invocation.
439 *
440 * @return int
441 * @access private
442 */
443 function stashSession() {
444 $stash = $this->saveTempUploadedFile(
445 $this->mUploadSaveName, $this->mUploadTempName );
446
447 if( !$stash ) {
448 # Couldn't save the file.
449 return false;
450 }
451
452 $key = mt_rand( 0, 0x7fffffff );
453 $_SESSION['wsUploadData'][$key] = array(
454 'mUploadTempName' => $stash,
455 'mUploadSize' => $this->mUploadSize,
456 'mOname' => $this->mOname );
457 return $key;
458 }
459
460 /**
461 * Remove a temporarily kept file stashed by saveTempUploadedFile().
462 * @access private
463 */
464 function unsaveUploadedFile() {
465 global $wgOut;
466 wfSuppressWarnings();
467 $success = unlink( $this->mUploadTempName );
468 wfRestoreWarnings();
469 if ( ! $success ) {
470 $wgOut->fileDeleteError( $this->mUploadTempName );
471 }
472 }
473
474 /* -------------------------------------------------------------- */
475
476 /**
477 * Show some text and linkage on successful upload.
478 * @access private
479 */
480 function showSuccess() {
481 global $wgUser, $wgOut, $wgContLang;
482
483 $sk = $wgUser->getSkin();
484 $ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) );
485 $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName;
486 $dlink = $sk->makeKnownLink( $dname, $dname );
487
488 $wgOut->addHTML( '<h2>' . wfMsgHtml( 'successfulupload' ) . "</h2>\n" );
489 $text = wfMsgWikiHtml( 'fileuploaded', $ilink, $dlink );
490 $wgOut->addHTML( $text );
491 $wgOut->returnToMain( false );
492 }
493
494 /**
495 * @param string $error as HTML
496 * @access private
497 */
498 function uploadError( $error ) {
499 global $wgOut;
500 $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
501 $wgOut->addHTML( "<span class='error'>{$error}</span>\n" );
502 }
503
504 /**
505 * There's something wrong with this file, not enough to reject it
506 * totally but we require manual intervention to save it for real.
507 * Stash it away, then present a form asking to confirm or cancel.
508 *
509 * @param string $warning as HTML
510 * @access private
511 */
512 function uploadWarning( $warning ) {
513 global $wgOut;
514 global $wgUseCopyrightUpload;
515
516 $this->mSessionKey = $this->stashSession();
517 if( !$this->mSessionKey ) {
518 # Couldn't save file; an error has been displayed so let's go.
519 return;
520 }
521
522 $wgOut->addHTML( "<h2>" . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
523 $wgOut->addHTML( "<ul class='warning'>{$warning}</ul><br />\n" );
524
525 $save = wfMsgHtml( 'savefile' );
526 $reupload = wfMsgHtml( 'reupload' );
527 $iw = wfMsgWikiHtml( 'ignorewarning' );
528 $reup = wfMsgWikiHtml( 'reuploaddesc' );
529 $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' );
530 $action = $titleObj->escapeLocalURL( 'action=submit' );
531
532 if ( $wgUseCopyrightUpload )
533 {
534 $copyright = "
535 <input type='hidden' name='wpUploadCopyStatus' value=\"" . htmlspecialchars( $this->mUploadCopyStatus ) . "\" />
536 <input type='hidden' name='wpUploadSource' value=\"" . htmlspecialchars( $this->mUploadSource ) . "\" />
537 ";
538 } else {
539 $copyright = "";
540 }
541
542 $wgOut->addHTML( "
543 <form id='uploadwarning' method='post' enctype='multipart/form-data' action='$action'>
544 <input type='hidden' name='wpIgnoreWarning' value='1' />
545 <input type='hidden' name='wpSessionKey' value=\"" . htmlspecialchars( $this->mSessionKey ) . "\" />
546 <input type='hidden' name='wpUploadDescription' value=\"" . htmlspecialchars( $this->mUploadDescription ) . "\" />
547 <input type='hidden' name='wpLicense' value=\"" . htmlspecialchars( $this->mLicense ) . "\" />
548 <input type='hidden' name='wpDestFile' value=\"" . htmlspecialchars( $this->mDestFile ) . "\" />
549 <input type='hidden' name='wpWatchthis' value=\"" . htmlspecialchars( intval( $this->mWatchthis ) ) . "\" />
550 {$copyright}
551 <table border='0'>
552 <tr>
553 <tr>
554 <td align='right'>
555 <input tabindex='2' type='submit' name='wpUpload' value=\"$save\" />
556 </td>
557 <td align='left'>$iw</td>
558 </tr>
559 <tr>
560 <td align='right'>
561 <input tabindex='2' type='submit' name='wpReUpload' value=\"{$reupload}\" />
562 </td>
563 <td align='left'>$reup</td>
564 </tr>
565 </tr>
566 </table></form>\n" );
567 }
568
569 /**
570 * Displays the main upload form, optionally with a highlighted
571 * error message up at the top.
572 *
573 * @param string $msg as HTML
574 * @access private
575 */
576 function mainUploadForm( $msg='' ) {
577 global $wgOut, $wgUser;
578 global $wgUseCopyrightUpload;
579
580 $cols = intval($wgUser->getOption( 'cols' ));
581 $ew = $wgUser->getOption( 'editwidth' );
582 if ( $ew ) $ew = " style=\"width:100%\"";
583 else $ew = '';
584
585 if ( '' != $msg ) {
586 $sub = wfMsgHtml( 'uploaderror' );
587 $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
588 "<span class='error'>{$msg}</span>\n" );
589 }
590 $wgOut->addHTML( '<div id="uploadtext">' );
591 $wgOut->addWikiText( wfMsg( 'uploadtext' ) );
592 $wgOut->addHTML( '</div>' );
593 $sk = $wgUser->getSkin();
594
595
596 $sourcefilename = wfMsgHtml( 'sourcefilename' );
597 $destfilename = wfMsgHtml( 'destfilename' );
598 $summary = wfMsgWikiHtml( 'fileuploadsummary' );
599
600 $licenses = new Licenses();
601 $license = wfMsgHtml( 'license' );
602 $nolicense = wfMsgHtml( 'nolicense' );
603 $licenseshtml = $licenses->getHtml();
604
605 $ulb = wfMsgHtml( 'uploadbtn' );
606
607
608 $titleObj = Title::makeTitle( NS_SPECIAL, 'Upload' );
609 $action = $titleObj->escapeLocalURL();
610
611 $encDestFile = htmlspecialchars( $this->mDestFile );
612
613 $watchChecked = $wgUser->getOption( 'watchdefault' )
614 ? 'checked="checked"'
615 : '';
616
617 $wgOut->addHTML( "
618 <form id='upload' method='post' enctype='multipart/form-data' action=\"$action\">
619 <table border='0'>
620 <tr>
621 <td align='right'><label for='wpUploadFile'>{$sourcefilename}:</label></td>
622 <td align='left'>
623 <input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . ($this->mDestFile?"":"onchange='fillDestFilename()' ") . "size='40' />
624 </td>
625 </tr>
626 <tr>
627 <td align='right'><label for='wpDestFile'>{$destfilename}:</label></td>
628 <td align='left'>
629 <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='40' value=\"$encDestFile\" />
630 </td>
631 </tr>
632 <tr>
633 <td align='right'><label for='wpUploadDescription'>{$summary}</label></td>
634 <td align='left'>
635 <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' cols='{$cols}'{$ew}>" . htmlspecialchars( $this->mUploadDescription ) . "</textarea>
636 </td>
637 </tr>
638 <tr>" );
639
640 if ( $licenseshtml != '' ) {
641 global $wgStylePath;
642 $wgOut->addHTML( "
643 <td align='right'><label for='wpLicense'>$license:</label></td>
644 <td align='left'>
645 <script type='text/javascript' src=\"$wgStylePath/common/upload.js\"></script>
646 <select name='wpLicense' id='wpLicense' tabindex='4'
647 onchange='licenseSelectorCheck()'>
648 <option value=''>$nolicense</option>
649 $licenseshtml
650 </select>
651 </td>
652 </tr>
653 <tr>
654 ");
655 }
656
657 if ( $wgUseCopyrightUpload ) {
658 $filestatus = wfMsgHtml ( 'filestatus' );
659 $copystatus = htmlspecialchars( $this->mUploadCopyStatus );
660 $filesource = wfMsgHtml ( 'filesource' );
661 $uploadsource = htmlspecialchars( $this->mUploadSource );
662
663 $wgOut->addHTML( "
664 <td align='right' nowrap='nowrap'><label for='wpUploadCopyStatus'>$filestatus:</label></td>
665 <td><input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus' value=\"$copystatus\" size='40' /></td>
666 </tr>
667 <tr>
668 <td align='right'><label for='wpUploadCopyStatus'>$filesource:</label></td>
669 <td><input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus' value=\"$uploadsource\" size='40' /></td>
670 </tr>
671 <tr>
672 ");
673 }
674
675
676 $wgOut->addHtml( "
677 <td></td>
678 <td>
679 <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
680 <label for='wpWatchthis'>" . wfMsgHtml( 'watchthis' ) . "</label>
681 <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' />
682 <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
683 </td>
684 </tr>
685 <tr>
686
687 </tr>
688 <tr>
689 <td></td>
690 <td align='left'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\" /></td>
691 </tr>
692
693 <tr>
694 <td></td>
695 <td align='left'>
696 " );
697 $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
698 $wgOut->addHTML( "
699 </td>
700 </tr>
701
702 </table>
703 </form>" );
704 }
705
706 /* -------------------------------------------------------------- */
707
708 /**
709 * Split a file into a base name and all dot-delimited 'extensions'
710 * on the end. Some web server configurations will fall back to
711 * earlier pseudo-'extensions' to determine type and execute
712 * scripts, so the blacklist needs to check them all.
713 *
714 * @return array
715 */
716 function splitExtensions( $filename ) {
717 $bits = explode( '.', $filename );
718 $basename = array_shift( $bits );
719 return array( $basename, $bits );
720 }
721
722 /**
723 * Perform case-insensitive match against a list of file extensions.
724 * Returns true if the extension is in the list.
725 *
726 * @param string $ext
727 * @param array $list
728 * @return bool
729 */
730 function checkFileExtension( $ext, $list ) {
731 return in_array( strtolower( $ext ), $list );
732 }
733
734 /**
735 * Perform case-insensitive match against a list of file extensions.
736 * Returns true if any of the extensions are in the list.
737 *
738 * @param array $ext
739 * @param array $list
740 * @return bool
741 */
742 function checkFileExtensionList( $ext, $list ) {
743 foreach( $ext as $e ) {
744 if( in_array( strtolower( $e ), $list ) ) {
745 return true;
746 }
747 }
748 return false;
749 }
750
751 /**
752 * Verifies that it's ok to include the uploaded file
753 *
754 * @param string $tmpfile the full path of the temporary file to verify
755 * @param string $extension The filename extension that the file is to be served with
756 * @return mixed true of the file is verified, a WikiError object otherwise.
757 */
758 function verify( $tmpfile, $extension ) {
759 #magically determine mime type
760 $magic=& wfGetMimeMagic();
761 $mime= $magic->guessMimeType($tmpfile,false);
762
763 $fname= "SpecialUpload::verify";
764
765 #check mime type, if desired
766 global $wgVerifyMimeType;
767 if ($wgVerifyMimeType) {
768
769 #check mime type against file extension
770 if( !$this->verifyExtension( $mime, $extension ) ) {
771 return new WikiErrorMsg( 'uploadcorrupt' );
772 }
773
774 #check mime type blacklist
775 global $wgMimeTypeBlacklist;
776 if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
777 && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
778 return new WikiErrorMsg( 'badfiletype', htmlspecialchars( $mime ) );
779 }
780 }
781
782 #check for htmlish code and javascript
783 if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
784 return new WikiErrorMsg( 'uploadscripted' );
785 }
786
787 /**
788 * Scan the uploaded file for viruses
789 */
790 $virus= $this->detectVirus($tmpfile);
791 if ( $virus ) {
792 return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
793 }
794
795 wfDebug( "$fname: all clear; passing.\n" );
796 return true;
797 }
798
799 /**
800 * Checks if the mime type of the uploaded file matches the file extension.
801 *
802 * @param string $mime the mime type of the uploaded file
803 * @param string $extension The filename extension that the file is to be served with
804 * @return bool
805 */
806 function verifyExtension( $mime, $extension ) {
807 $fname = 'SpecialUpload::verifyExtension';
808
809 $magic =& wfGetMimeMagic();
810
811 if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
812 if ( ! $magic->isRecognizableExtension( $extension ) ) {
813 wfDebug( "$fname: passing file with unknown detected mime type; unrecognized extension '$extension', can't verify\n" );
814 return true;
815 } else {
816 wfDebug( "$fname: rejecting file with unknown detected mime type; recognized extension '$extension', so probably invalid file\n" );
817 return false;
818 }
819
820 $match= $magic->isMatchingExtension($extension,$mime);
821
822 if ($match===NULL) {
823 wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" );
824 return true;
825 } elseif ($match===true) {
826 wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" );
827
828 #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
829 return true;
830
831 } else {
832 wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" );
833 return false;
834 }
835 }
836
837 /** Heuristig for detecting files that *could* contain JavaScript instructions or
838 * things that may look like HTML to a browser and are thus
839 * potentially harmful. The present implementation will produce false positives in some situations.
840 *
841 * @param string $file Pathname to the temporary upload file
842 * @param string $mime The mime type of the file
843 * @param string $extension The extension of the file
844 * @return bool true if the file contains something looking like embedded scripts
845 */
846 function detectScript($file, $mime, $extension) {
847 global $wgAllowTitlesInSVG;
848
849 #ugly hack: for text files, always look at the entire file.
850 #For binarie field, just check the first K.
851
852 if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
853 else {
854 $fp = fopen( $file, 'rb' );
855 $chunk = fread( $fp, 1024 );
856 fclose( $fp );
857 }
858
859 $chunk= strtolower( $chunk );
860
861 if (!$chunk) return false;
862
863 #decode from UTF-16 if needed (could be used for obfuscation).
864 if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
865 elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
866 else $enc= NULL;
867
868 if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
869
870 $chunk= trim($chunk);
871
872 #FIXME: convert from UTF-16 if necessarry!
873
874 wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
875
876 #check for HTML doctype
877 if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
878
879 /**
880 * Internet Explorer for Windows performs some really stupid file type
881 * autodetection which can cause it to interpret valid image files as HTML
882 * and potentially execute JavaScript, creating a cross-site scripting
883 * attack vectors.
884 *
885 * Apple's Safari browser also performs some unsafe file type autodetection
886 * which can cause legitimate files to be interpreted as HTML if the
887 * web server is not correctly configured to send the right content-type
888 * (or if you're really uploading plain text and octet streams!)
889 *
890 * Returns true if IE is likely to mistake the given file for HTML.
891 * Also returns true if Safari would mistake the given file for HTML
892 * when served with a generic content-type.
893 */
894
895 $tags = array(
896 '<body',
897 '<head',
898 '<html', #also in safari
899 '<img',
900 '<pre',
901 '<script', #also in safari
902 '<table'
903 );
904 if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
905 $tags[] = '<title';
906 }
907
908 foreach( $tags as $tag ) {
909 if( false !== strpos( $chunk, $tag ) ) {
910 return true;
911 }
912 }
913
914 /*
915 * look for javascript
916 */
917
918 #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
919 $chunk = Sanitizer::decodeCharReferences( $chunk );
920
921 #look for script-types
922 if (preg_match("!type\s*=\s*['\"]?\s*(\w*/)?(ecma|java)!sim",$chunk)) return true;
923
924 #look for html-style script-urls
925 if (preg_match("!(href|src|data)\s*=\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true;
926
927 #look for css-style script-urls
928 if (preg_match("!url\s*\(\s*['\"]?\s*(ecma|java)script:!sim",$chunk)) return true;
929
930 wfDebug("SpecialUpload::detectScript: no scripts found\n");
931 return false;
932 }
933
934 /** Generic wrapper function for a virus scanner program.
935 * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
936 * $wgAntivirusRequired may be used to deny upload if the scan fails.
937 *
938 * @param string $file Pathname to the temporary upload file
939 * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
940 * or a string containing feedback from the virus scanner if a virus was found.
941 * If textual feedback is missing but a virus was found, this function returns true.
942 */
943 function detectVirus($file) {
944 global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
945
946 $fname= "SpecialUpload::detectVirus";
947
948 if (!$wgAntivirus) { #disabled?
949 wfDebug("$fname: virus scanner disabled\n");
950
951 return NULL;
952 }
953
954 if (!$wgAntivirusSetup[$wgAntivirus]) {
955 wfDebug("$fname: unknown virus scanner: $wgAntivirus\n");
956
957 $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" ); #LOCALIZE
958
959 return "unknown antivirus: $wgAntivirus";
960 }
961
962 #look up scanner configuration
963 $virus_scanner= $wgAntivirusSetup[$wgAntivirus]["command"]; #command pattern
964 $virus_scanner_codes= $wgAntivirusSetup[$wgAntivirus]["codemap"]; #exit-code map
965 $msg_pattern= $wgAntivirusSetup[$wgAntivirus]["messagepattern"]; #message pattern
966
967 $scanner= $virus_scanner; #copy, so we can resolve the pattern
968
969 if (strpos($scanner,"%f")===false) $scanner.= " ".wfEscapeShellArg($file); #simple pattern: append file to scan
970 else $scanner= str_replace("%f",wfEscapeShellArg($file),$scanner); #complex pattern: replace "%f" with file to scan
971
972 wfDebug("$fname: running virus scan: $scanner \n");
973
974 #execute virus scanner
975 $code= false;
976
977 #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
978 # that does not seem to be worth the pain.
979 # Ask me (Duesentrieb) about it if it's ever needed.
980 if (wfIsWindows()) exec("$scanner",$output,$code);
981 else exec("$scanner 2>&1",$output,$code);
982
983 $exit_code= $code; #remeber for user feedback
984
985 if ($virus_scanner_codes) { #map exit code to AV_xxx constants.
986 if (isset($virus_scanner_codes[$code])) $code= $virus_scanner_codes[$code]; #explicite mapping
987 else if (isset($virus_scanner_codes["*"])) $code= $virus_scanner_codes["*"]; #fallback mapping
988 }
989
990 if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes)
991 wfDebug("$fname: failed to scan $file (code $exit_code).\n");
992
993 if ($wgAntivirusRequired) return "scan failed (code $exit_code)";
994 else return NULL;
995 }
996 else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune)
997 wfDebug("$fname: unsupported file type $file (code $exit_code).\n");
998 return NULL;
999 }
1000 else if ($code===AV_NO_VIRUS) {
1001 wfDebug("$fname: file passed virus scan.\n");
1002 return false; #no virus found
1003 }
1004 else {
1005 $output= join("\n",$output);
1006 $output= trim($output);
1007
1008 if (!$output) $output= true; #if ther's no output, return true
1009 else if ($msg_pattern) {
1010 $groups= array();
1011 if (preg_match($msg_pattern,$output,$groups)) {
1012 if ($groups[1]) $output= $groups[1];
1013 }
1014 }
1015
1016 wfDebug("$fname: FOUND VIRUS! scanner feedback: $output");
1017 return $output;
1018 }
1019 }
1020
1021 /**
1022 * Check if the temporary file is MacBinary-encoded, as some uploads
1023 * from Internet Explorer on Mac OS Classic and Mac OS X will be.
1024 * If so, the data fork will be extracted to a second temporary file,
1025 * which will then be checked for validity and either kept or discarded.
1026 *
1027 * @access private
1028 */
1029 function checkMacBinary() {
1030 $macbin = new MacBinary( $this->mUploadTempName );
1031 if( $macbin->isValid() ) {
1032 $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
1033 $dataHandle = fopen( $dataFile, 'wb' );
1034
1035 wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
1036 $macbin->extractData( $dataHandle );
1037
1038 $this->mUploadTempName = $dataFile;
1039 $this->mUploadSize = $macbin->dataForkLength();
1040
1041 // We'll have to manually remove the new file if it's not kept.
1042 $this->mRemoveTempFile = true;
1043 }
1044 $macbin->close();
1045 }
1046
1047 /**
1048 * If we've modified the upload file we need to manually remove it
1049 * on exit to clean up.
1050 * @access private
1051 */
1052 function cleanupTempFile() {
1053 if( $this->mRemoveTempFile && file_exists( $this->mUploadTempName ) ) {
1054 wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file $this->mUploadTempName\n" );
1055 unlink( $this->mUploadTempName );
1056 }
1057 }
1058
1059 /**
1060 * Check if there's an overwrite conflict and, if so, if restrictions
1061 * forbid this user from performing the upload.
1062 *
1063 * @return mixed true on success, WikiError on failure
1064 * @access private
1065 */
1066 function checkOverwrite( $name ) {
1067 $img = Image::newFromName( $name );
1068 if( is_null( $img ) ) {
1069 // Uh... this shouldn't happen ;)
1070 // But if it does, fall through to previous behavior
1071 return false;
1072 }
1073
1074 $error = '';
1075 if( $img->exists() ) {
1076 global $wgUser, $wgOut;
1077 if( $img->isLocal() ) {
1078 if( !$wgUser->isAllowed( 'reupload' ) ) {
1079 $error = 'fileexists-forbidden';
1080 }
1081 } else {
1082 if( !$wgUser->isAllowed( 'reupload' ) ||
1083 !$wgUser->isAllowed( 'reupload-shared' ) ) {
1084 $error = "fileexists-shared-forbidden";
1085 }
1086 }
1087 }
1088
1089 if( $error ) {
1090 $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
1091 return new WikiError( $wgOut->parse( $errorText ) );
1092 }
1093
1094 // Rockin', go ahead and upload
1095 return true;
1096 }
1097
1098 }
1099 ?>