From 85fa7504e68ea6a67cc7bb0b34de3dce2868f11f Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Sat, 16 Jun 2007 02:55:25 +0000 Subject: [PATCH] * Split off ultimate base class FileRepo from FSRepo * Use rawurlencode() for paths instead of urlencode() * Moved functionality of LocalFile::loadFromFile() to a public static function. This allows Special:Upload to entirely avoid loading metadata from the (potentially remote) store. Changed purgeMetadataCache() to use loadFromDB() instead of loadFromFile(). * Redefined private virtual URLs, giving them a repo name component instead of a host. * Moved is_writable($wgUploadDirectory) in SpecialUpload.php to the repo * Added some filename validation to the FSRepo's store/publish * Changed store and publish to use relative destinations instead of absolute, as was documented * Deprecated File::recordUpload()! Now use upload() to do both publish and record. Moved the UI bits to SpecialUpload. * Create a null revision on reupload * Changed most of the member variable names in UploadForm. $this->mUpload followed by some ambiguous word or abbreviation was not a good naming convention. Also did some reformatting and assorted code cleanup. --- includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 26 +- includes/ImagePage.php | 3 +- includes/LogPage.php | 20 +- includes/SpecialUpload.php | 518 +++++++++++--------- includes/filerepo/FSRepo.php | 226 ++------- includes/filerepo/File.php | 74 ++- includes/filerepo/FileRepo.php | 280 +++++++++++ includes/filerepo/ForeignDBRepo.php | 2 +- includes/filerepo/LocalFile.php | 206 ++++---- includes/filerepo/RepoGroup.php | 29 ++ includes/filerepo/UnregisteredLocalFile.php | 2 +- tests/LocalFileTest.php | 16 +- 13 files changed, 824 insertions(+), 579 deletions(-) create mode 100644 includes/filerepo/FileRepo.php diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 849be069c8..ea89ea8e8f 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -254,6 +254,7 @@ function __autoload($className) { # filerepo 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', 'File' => 'includes/filerepo/File.php', + 'FileRepo' => 'includes/filerepo/FileRepo.php', 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', 'FSRepo' => 'includes/filerepo/FSRepo.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 82aff9ddb7..0ecd8a00e1 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -308,34 +308,34 @@ $wgAntivirus= NULL; * * @global array $wgAntivirusSetup */ -$wgAntivirusSetup= array( +$wgAntivirusSetup = array( #setup for clamav 'clamav' => array ( 'command' => "clamscan --no-summary ", - 'codemap'=> array ( - "0"=> AV_NO_VIRUS, #no virus - "1"=> AV_VIRUS_FOUND, #virus found - "52"=> AV_SCAN_ABORTED, #unsupported file format (probably imune) - "*"=> AV_SCAN_FAILED, #else scan failed + 'codemap' => array ( + "0" => AV_NO_VIRUS, # no virus + "1" => AV_VIRUS_FOUND, # virus found + "52" => AV_SCAN_ABORTED, # unsupported file format (probably imune) + "*" => AV_SCAN_FAILED, # else scan failed ), - 'messagepattern'=> '/.*?:(.*)/sim', + 'messagepattern' => '/.*?:(.*)/sim', ), #setup for f-prot 'f-prot' => array ( 'command' => "f-prot ", - 'codemap'=> array ( - "0"=> AV_NO_VIRUS, #no virus - "3"=> AV_VIRUS_FOUND, #virus found - "6"=> AV_VIRUS_FOUND, #virus found - "*"=> AV_SCAN_FAILED, #else scan failed + 'codemap' => array ( + "0" => AV_NO_VIRUS, # no virus + "3" => AV_VIRUS_FOUND, # virus found + "6" => AV_VIRUS_FOUND, # virus found + "*" => AV_SCAN_FAILED, # else scan failed ), - 'messagepattern'=> '/.*?Infection:(.*)$/m', + 'messagepattern' => '/.*?Infection:(.*)$/m', ), ); diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 1f3454168d..ee408a5004 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -664,7 +664,8 @@ EOT } $sourcePath = $this->img->getArchiveVirtualUrl( $oldimage ); - $result = $this->img->publish( $sourcePath ); + $comment = wfMsg( "reverted" ); + $result = $this->img->upload( $sourcePath, $comment, $comment ); if ( WikiError::isError( $result ) ) { $this->showError( $result ); diff --git a/includes/LogPage.php b/includes/LogPage.php index b8ef781e1f..02cde2af49 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -79,20 +79,24 @@ class LogPage { # And update recentchanges if ( $this->updateRecentChanges ) { $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); - $rcComment = $this->actionText; - if( '' != $this->comment ) { - if ($rcComment == '') - $rcComment = $this->comment; - else - $rcComment .= ': ' . $this->comment; - } - + $rcComment = $this->getRcComment(); RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '', $this->type, $this->action, $this->target, $this->comment, $this->params ); } return true; } + public function getRcComment() { + $rcComment = $this->actionText; + if( '' != $this->comment ) { + if ($rcComment == '') + $rcComment = $this->comment; + else + $rcComment .= ': ' . $this->comment; + } + return $rcComment; + } + /** * @static */ diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index 2ebd3fa7ad..38e670e65a 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -22,18 +22,19 @@ class UploadForm { /**#@+ * @access private */ - var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError; - var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion; - var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload; - var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType; - var $mUploadTempFileSize = 0; - var $mImage; + var $mComment, $mLicense, $mIgnoreWarning, $mCurlError; + var $mDestName, $mTempPath, $mFileSize, $mFileProps; + var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked; + var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType; + var $mCurlDestHandle; + var $mLocalFile; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value var $uploadFormTextTop; var $uploadFormTextAfterSummary; + const SESSION_VERSION = 1; /**#@-*/ /** @@ -43,7 +44,7 @@ class UploadForm { */ function UploadForm( &$request ) { global $wgAllowCopyUploads; - $this->mDestFile = $request->getText( 'wpDestFile' ); + $this->mDesiredDestName = $request->getText( 'wpDestFile' ); if( !$request->wasPosted() ) { # GET requests just give the main form; no data except wpDestfile. @@ -56,21 +57,22 @@ class UploadForm { $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); $this->mReUpload = $request->getCheck( 'wpReUpload' ); - $this->mUpload = $request->getCheck( 'wpUpload' ); + $this->mUploadClicked = $request->getCheck( 'wpUpload' ); - $this->mUploadDescription = $request->getText( 'wpUploadDescription' ); + $this->mComment = $request->getText( 'wpUploadDescription' ); $this->mLicense = $request->getText( 'wpLicense' ); - $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' ); - $this->mUploadSource = $request->getText( 'wpUploadSource' ); + $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); + $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); $this->mWatchthis = $request->getBool( 'wpWatchthis' ); - $this->mSourceType = $request->getText( 'wpSourceType' ); + $this->mSourceType = $request->getText( 'wpSourceType' ); wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" ); $this->mAction = $request->getVal( 'action' ); $this->mSessionKey = $request->getInt( 'wpSessionKey' ); if( !empty( $this->mSessionKey ) && - isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) { + isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) && + $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) { /** * Confirming a temporarily stashed upload. * We don't want path names to be forged, so we keep @@ -78,10 +80,11 @@ class UploadForm { * an opaque key to the user agent. */ $data = $_SESSION['wsUploadData'][$this->mSessionKey]; - $this->mUploadTempName = $data['mUploadTempName']; - $this->mUploadSize = $data['mUploadSize']; - $this->mOname = $data['mOname']; - $this->mUploadError = 0/*UPLOAD_ERR_OK*/; + $this->mTempPath = $data['mTempPath']; + $this->mFileSize = $data['mFileSize']; + $this->mSrcName = $data['mSrcName']; + $this->mFileProps = $data['mFileProps']; + $this->mCurlError = 0/*UPLOAD_ERR_OK*/; $this->mStashed = true; $this->mRemoveTempFile = false; } else { @@ -101,10 +104,11 @@ class UploadForm { * @access private */ function initializeFromUpload( $request ) { - $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' ); - $this->mUploadSize = $request->getFileSize( 'wpUploadFile' ); - $this->mOname = $request->getFileName( 'wpUploadFile' ); - $this->mUploadError = $request->getUploadError( 'wpUploadFile' ); + $this->mTempPath = $request->getFileTempName( 'wpUploadFile' ); + $this->mFileSize = $request->getFileSize( 'wpUploadFile' ); + $this->mSrcName = $request->getFileName( 'wpUploadFile' ); + $this->mCurlError = $request->getUploadError( 'wpUploadFile' ); + $this->mFileProps = File::getPropsFromPath( $this->mTempPath ); $this->mSessionKey = false; $this->mStashed = false; $this->mRemoveTempFile = false; // PHP will handle this @@ -119,10 +123,10 @@ class UploadForm { $url = $request->getText( 'wpUploadFileURL' ); $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); - $this->mUploadTempName = $local_file; - $this->mUploadError = $this->curlCopy( $url, $local_file ); - $this->mUploadSize = $this->mUploadTempFileSize; - $this->mOname = array_pop( explode( '/', $url ) ); + $this->mTempPath = $local_file; + $this->mFileSize = 0; # Will be set by curlCopy + $this->mCurlError = $this->curlCopy( $url, $local_file ); + $this->mSrcName = array_pop( explode( '/', $url ) ); $this->mSessionKey = false; $this->mStashed = false; @@ -151,8 +155,8 @@ class UploadForm { } # Open temporary file - $this->mUploadTempFile = @fopen( $this->mUploadTempName, "wb" ); - if( $this->mUploadTempFile === false ) { + $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); + if( $this->mCurlDestHandle === false ) { # Could not open temporary file to write in $wgOut->errorPage( 'upload-file-error', 'upload-file-error-text'); return true; @@ -170,8 +174,8 @@ class UploadForm { // if ( $error ) print curl_error ( $ch ) ; # Debugging output curl_close( $ch ); - fclose( $this->mUploadTempFile ); - unset( $this->mUploadTempFile ); + fclose( $this->mCurlDestHandle ); + unset( $this->mCurlDestHandle ); if( $error ) { unlink( $dest ); if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) ) @@ -192,11 +196,11 @@ class UploadForm { function uploadCurlCallback( $ch, $data ) { global $wgMaxUploadSize; $length = strlen( $data ); - $this->mUploadTempFileSize += $length; - if( $this->mUploadTempFileSize > $wgMaxUploadSize ) { + $this->mFileSize += $length; + if( $this->mFileSize > $wgMaxUploadSize ) { return 0; } - fwrite( $this->mUploadTempFile, $data ); + fwrite( $this->mCurlDestHandle, $data ); return $length; } @@ -206,11 +210,11 @@ class UploadForm { */ function execute() { global $wgUser, $wgOut; - global $wgEnableUploads, $wgUploadDirectory; + global $wgEnableUploads; # Check uploading enabled if( !$wgEnableUploads ) { - $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDestFile ) ); + $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) ); return; } @@ -235,18 +239,12 @@ class UploadForm { return; } - /** Check if the image directory is writeable, this is a common mistake */ - if( !is_writeable( $wgUploadDirectory ) ) { - $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) ); - return; - } - if( $this->mReUpload ) { if( !$this->unsaveUploadedFile() ) { return; } $this->mainUploadForm(); - } else if( 'submit' == $this->mAction || $this->mUpload ) { + } else if( 'submit' == $this->mAction || $this->mUploadClicked ) { $this->processUpload(); } else { $this->mainUploadForm(); @@ -272,7 +270,7 @@ class UploadForm { } /* Check for PHP error if any, requires php 4.2 or newer */ - if( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { + if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); return; } @@ -280,16 +278,16 @@ class UploadForm { /** * If there was no filename or a zero size given, give up quick. */ - if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) { + if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); return; } # Chop off any directories in the given filename - if( $this->mDestFile ) { - $basename = wfBaseName( $this->mDestFile ); + if( $this->mDesiredDestName ) { + $basename = wfBaseName( $this->mDesiredDestName ); } else { - $basename = wfBaseName( $this->mOname ); + $basename = wfBaseName( $this->mSrcName ); } /** @@ -321,13 +319,13 @@ class UploadForm { * out of it. We'll strip some silently that Title would die on. */ $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename ); - $nt = Title::newFromText( $filtered ); + $nt = Title::makeTitleSafe( NS_IMAGE, $filtered ); if( is_null( $nt ) ) { $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) ); return; } - $nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() ); - $this->mUploadSaveName = $nt->getDBkey(); + $this->mLocalFile = wfLocalFile( $nt ); + $this->mDestName = $this->mLocalFile->getName(); /** * If the image is protected, non-sysop users won't be able @@ -340,7 +338,7 @@ class UploadForm { /** * In some cases we may forbid overwriting of existing files. */ - $overwrite = $this->checkOverwrite( $this->mUploadSaveName ); + $overwrite = $this->checkOverwrite( $this->mDestName ); if( WikiError::isError( $overwrite ) ) { return $this->uploadError( $overwrite->toString() ); } @@ -351,9 +349,9 @@ class UploadForm { if ($finalExt == '') { return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) ); } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || - ($wgStrictFileExtensions && - !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) ); + ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { + return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), + htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) ); } /** @@ -363,7 +361,7 @@ class UploadForm { */ if( !$this->mStashed ) { $this->checkMacBinary(); - $veri = $this->verify( $this->mUploadTempName, $finalExt ); + $veri = $this->verify( $this->mTempPath, $finalExt ); if( $veri !== true ) { //it's a wiki error... return $this->uploadError( $veri->toString() ); @@ -374,7 +372,7 @@ class UploadForm { */ $error = ''; if( !wfRunHooks( 'UploadVerification', - array( $this->mUploadSaveName, $this->mUploadTempName, &$error ) ) ) { + array( $this->mDestName, $this->mTempPath, &$error ) ) ) { return $this->uploadError( $error ); } } @@ -390,31 +388,31 @@ class UploadForm { if( $wgCapitalLinks ) { $filtered = ucfirst( $filtered ); } - if( $this->mUploadSaveName != $filtered ) { - $warning .= '
  • '.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mUploadSaveName ) ).'
  • '; + if( $this->mDestName != $filtered ) { + $warning .= '
  • '.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'
  • '; } global $wgCheckFileExtensions; if ( $wgCheckFileExtensions ) { if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - $warning .= '
  • '.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'
  • '; + $warning .= '
  • '.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), + htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'
  • '; } } global $wgUploadSizeWarning; - if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) { + if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { $skin = $wgUser->getSkin(); $wsize = $skin->formatSize( $wgUploadSizeWarning ); - $asize = $skin->formatSize( $this->mUploadSize ); + $asize = $skin->formatSize( $this->mFileSize ); $warning .= '
  • ' . wfMsgHtml( 'large-file', $wsize, $asize ) . '
  • '; } - if ( $this->mUploadSize == 0 ) { + if ( $this->mFileSize == 0 ) { $warning .= '
  • '.wfMsgHtml( 'emptyfile' ).'
  • '; } global $wgUser; $sk = $wgUser->getSkin(); - $image = wfLocalFile( $nt ); // Check for uppercase extension. We allow these filenames but check if an image // with lowercase extension exists already @@ -423,13 +421,15 @@ class UploadForm { $image_lc = wfLocalFile( $nt_lc ); } - if( $image->exists() ) { + if( $this->mLocalFile->exists() ) { $dlink = $sk->makeKnownLinkObj( $nt ); - if ( $image->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt->getText(), 'right', array(), false, true ); - } elseif ( !$image->allowInlineDisplay() && $image->isSafeFile() ) { - $icon = $image->iconThumb(); - $dlink2 = '
    ' . $icon->toHtml() . '
    ' . $dlink . '
    '; + if ( $this->mLocalFile->allowInlineDisplay() ) { + $dlink2 = $sk->makeImageLinkObj( $nt, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $nt->getText(), 'right', array(), false, true ); + } elseif ( !$this->mLocalFile->allowInlineDisplay() && $this->mLocalFile->isSafeFile() ) { + $icon = $this->mLocalFile->iconThumb(); + $dlink2 = '
    ' . + $icon->toHtml() . '
    ' . $dlink . '
    '; } else { $dlink2 = ''; } @@ -441,17 +441,22 @@ class UploadForm { # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension $dlink = $sk->makeKnownLinkObj( $nt_lc ); if ( $image_lc->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_lc->getText(), 'right', array(), false, true ); + $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $nt_lc->getText(), 'right', array(), false, true ); } elseif ( !$image_lc->allowInlineDisplay() && $image_lc->isSafeFile() ) { $icon = $image_lc->iconThumb(); - $dlink2 = '
    ' . $icon->toHtml() . '
    ' . $dlink . '
    '; + $dlink2 = '
    ' . + $icon->toHtml() . '
    ' . $dlink . '
    '; } else { $dlink2 = ''; } - $warning .= '
  • ' . wfMsgExt( 'fileexists-extension', 'parsemag' , $partname . '.' . $finalExt , $dlink ) . '
  • ' . $dlink2; + $warning .= '
  • ' . wfMsgExt( 'fileexists-extension', 'parsemag' , $partname . '.' + . $finalExt , $dlink ) . '
  • ' . $dlink2; - } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) { + } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) + && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) + { # Check for filenames like 50px- or 180px-, these are mostly thumbnails $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $finalExt ); $image_thb = wfLocalFile( $nt_thb ); @@ -459,26 +464,34 @@ class UploadForm { # Check if an image without leading '180px-' (or similiar) exists $dlink = $sk->makeKnownLinkObj( $nt_thb); if ( $image_thb->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_thb, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_thb->getText(), 'right', array(), false, true ); + $dlink2 = $sk->makeImageLinkObj( $nt_thb, + wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $nt_thb->getText(), 'right', array(), false, true ); } elseif ( !$image_thb->allowInlineDisplay() && $image_thb->isSafeFile() ) { $icon = $image_thb->iconThumb(); - $dlink2 = '
    ' . $icon->toHtml() . '
    ' . $dlink . '
    '; + $dlink2 = '
    ' . $icon->toHtml() . '
    ' . + $dlink . '
    '; } else { $dlink2 = ''; } - $warning .= '
  • ' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . '
  • ' . $dlink2; + $warning .= '
  • ' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . + '
  • ' . $dlink2; } else { # Image w/o '180px-' does not exists, but we do not like these filenames - $warning .= '
  • ' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '
  • '; + $warning .= '
  • ' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , + substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '
  • '; } } - if ( $image->wasDeleted() ) { + if ( $this->mLocalFile->wasDeleted() ) { # If the file existed before and was deleted, warn the user of this # Don't bother doing so if the image exists now, however $ltitle = SpecialPage::getTitleFor( 'Log' ); - $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() ); - $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' ); + $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), + 'type=delete&page=' . $nt->getPrefixedUrl() ); + $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . + wfCloseElement( 'li' ); } if( $warning != '' ) { @@ -494,56 +507,21 @@ class UploadForm { * Try actually saving the thing... * It will show an error form on failure. */ - $hasBeenMunged = !empty( $this->mSessionKey ) || $this->mRemoveTempFile; - if( $this->saveUploadedFile( $this->mUploadSaveName, - $this->mUploadTempName, - $hasBeenMunged ) ) { - /** - * Update the upload log and create the description page - * if it's a new file. - */ - $this->mImage = wfLocalFile( $this->mUploadSaveName ); - $success = $this->mImage->recordUpload( $this->mUploadOldVersion, - $this->mUploadDescription, - $this->mLicense, - $this->mUploadCopyStatus, - $this->mUploadSource, - $this->mWatchthis ); - - if ( $success ) { - $this->showSuccess(); - wfRunHooks( 'UploadComplete', array( &$img ) ); - } else { - // File::recordUpload() fails if the image went missing, which is - // unlikely, hence the lack of a specialised message - $wgOut->showFileNotFoundError( $this->mUploadSaveName ); - } - } - } - - /** - * Move the uploaded file from its temporary location to the final - * destination. If a previous version of the file exists, move - * it into the archive subdirectory. - * - * @todo If the later save fails, we may have disappeared the original file. - * - * @param string $saveName - * @param string $tempName full path to the temporary file - * @param bool $useRename if true, doesn't check that the source file - * is a PHP-managed upload temporary - */ - function saveUploadedFile( $saveName, $tempName, $useRename = false ) { - global $wgOut, $wgAllowCopyUploads; + $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, + $this->mCopyrightStatus, $this->mCopyrightSource ); - $image = wfLocalFile( $saveName ); - $archiveName = $image->publish( $tempName, File::DELETE_SOURCE ); - if ( WikiError::isError( $archiveName ) ) { - $this->showError( $archiveName ); - return false; + $error = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, + File::DELETE_SOURCE, $this->mFileProps ); + if ( WikiError::isError( $error ) ) { + $this->showError( $error ); + } else { + if ( $this->mWatchthis ) { + global $wgUser; + $wgUser->addWatch( $this->mLocalFile->getTitle() ); + } + $this->showSuccess(); + wfRunHooks( 'UploadComplete', array( &$img ) ); } - $this->mUploadOldVersion = $archiveName; - return true; } /** @@ -580,8 +558,7 @@ class UploadForm { * @access private */ function stashSession() { - $stash = $this->saveTempUploadedFile( - $this->mUploadSaveName, $this->mUploadTempName ); + $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); if( !$stash ) { # Couldn't save the file. @@ -590,9 +567,12 @@ class UploadForm { $key = mt_rand( 0, 0x7fffffff ); $_SESSION['wsUploadData'][$key] = array( - 'mUploadTempName' => $stash, - 'mUploadSize' => $this->mUploadSize, - 'mOname' => $this->mOname ); + 'mTempPath' => $stash, + 'mFileSize' => $this->mFileSize, + 'mSrcName' => $this->mSrcName, + 'mFileProps' => $this->mFileProps, + 'version' => self::SESSION_VERSION, + ); return $key; } @@ -604,9 +584,9 @@ class UploadForm { function unsaveUploadedFile() { global $wgOut; $repo = RepoGroup::singleton()->getLocalRepo(); - $success = $repo->freeTemp( $this->mUploadTempName ); + $success = $repo->freeTemp( $this->mTempPath ); if ( ! $success ) { - $wgOut->showFileDeleteError( $this->mUploadTempName ); + $wgOut->showFileDeleteError( $this->mTempPath ); return false; } else { return true; @@ -623,8 +603,8 @@ class UploadForm { global $wgUser, $wgOut, $wgContLang; $sk = $wgUser->getSkin(); - $ilink = $sk->makeMediaLinkObj( $this->mImage->getTitle() ); - $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName; + $ilink = $sk->makeMediaLinkObj( $this->mLocalFile->getTitle() ); + $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mDestName; $dlink = $sk->makeKnownLink( $dname, $dname ); $wgOut->addHTML( '

    ' . wfMsgHtml( 'successfulupload' ) . "

    \n" ); @@ -674,8 +654,8 @@ class UploadForm { if ( $wgUseCopyrightUpload ) { $copyright = " - mUploadCopyStatus ) . "\" /> - mUploadSource ) . "\" /> + mCopyrightStatus ) . "\" /> + mCopyrightSource ) . "\" /> "; } else { $copyright = ""; @@ -685,9 +665,9 @@ class UploadForm {
    mSessionKey ) . "\" /> - mUploadDescription ) . "\" /> + mComment ) . "\" /> mLicense ) . "\" /> - mDestFile ) . "\" /> + mDesiredDestName ) . "\" /> mWatchthis ) ) . "\" /> {$copyright} @@ -737,7 +717,7 @@ class UploadForm { "{$msg}\n" ); } $wgOut->addHTML( '
    ' ); - $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDestFile ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) ); $wgOut->addHTML( '
    ' ); $sourcefilename = wfMsgHtml( 'sourcefilename' ); @@ -755,35 +735,44 @@ class UploadForm { $titleObj = SpecialPage::getTitleFor( 'Upload' ); $action = $titleObj->escapeLocalURL(); - $encDestFile = htmlspecialchars( $this->mDestFile ); + $encDestName = htmlspecialchars( $this->mDesiredDestName ); $watchChecked = ( $wgUser->getOption( 'watchdefault' ) || - ( $wgUser->getOption( 'watchcreations' ) && $this->mDestFile == '' ) ) + ( $wgUser->getOption( 'watchcreations' ) && $this->mDesiredDestName == '' ) ) ? 'checked="checked"' : ''; // Prepare form for upload or upload/copy if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) { $filename_form = - "" . - "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . + "" . + "mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . wfMsgHTML( 'upload_source_file' ) . "
    " . - "" . - "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" . + "" . + "mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" . wfMsgHtml( 'upload_source_url' ) ; } else { $filename_form = "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . + ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . "" ; } + $encComment = htmlspecialchars( $this->mComment ); - $wgOut->addHTML( " - + $wgOut->addHTML( <<
    {$this->uploadFormTextTop} @@ -795,17 +784,20 @@ class UploadForm { - " ); + +EOT + ); if ( $licenseshtml != '' ) { global $wgStylePath; @@ -826,17 +818,19 @@ class UploadForm { if ( $wgUseCopyrightUpload ) { $filestatus = wfMsgHtml ( 'filestatus' ); - $copystatus = htmlspecialchars( $this->mUploadCopyStatus ); + $copystatus = htmlspecialchars( $this->mCopyrightStatus ); $filesource = wfMsgHtml ( 'filesource' ); - $uploadsource = htmlspecialchars( $this->mUploadSource ); + $uploadsource = htmlspecialchars( $this->mCopyrightSource ); $wgOut->addHTML( " - + - + "); @@ -927,8 +921,6 @@ class UploadForm { $magic=& MimeMagic::singleton(); $mime= $magic->guessMimeType($tmpfile,false); - $fname= "SpecialUpload::verify"; - #check mime type, if desired global $wgVerifyMimeType; if ($wgVerifyMimeType) { @@ -959,7 +951,7 @@ class UploadForm { return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) ); } - wfDebug( "$fname: all clear; passing.\n" ); + wfDebug( __METHOD__.": all clear; passing.\n" ); return true; } @@ -971,45 +963,46 @@ class UploadForm { * @return bool */ function verifyExtension( $mime, $extension ) { - $fname = 'SpecialUpload::verifyExtension'; - $magic =& MimeMagic::singleton(); if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) if ( ! $magic->isRecognizableExtension( $extension ) ) { - wfDebug( "$fname: passing file with unknown detected mime type; unrecognized extension '$extension', can't verify\n" ); + wfDebug( __METHOD__.": passing file with unknown detected mime type; " . + "unrecognized extension '$extension', can't verify\n" ); return true; } else { - wfDebug( "$fname: rejecting file with unknown detected mime type; recognized extension '$extension', so probably invalid file\n" ); + wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". + "recognized extension '$extension', so probably invalid file\n" ); return false; } $match= $magic->isMatchingExtension($extension,$mime); if ($match===NULL) { - wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" ); + wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); return true; } elseif ($match===true) { - wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" ); + wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! return true; } else { - wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" ); + wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); return false; } } - /** Heuristig for detecting files that *could* contain JavaScript instructions or - * things that may look like HTML to a browser and are thus - * potentially harmful. The present implementation will produce false positives in some situations. - * - * @param string $file Pathname to the temporary upload file - * @param string $mime The mime type of the file - * @param string $extension The extension of the file - * @return bool true if the file contains something looking like embedded scripts - */ + /** + * Heuristic for detecting files that *could* contain JavaScript instructions or + * things that may look like HTML to a browser and are thus + * potentially harmful. The present implementation will produce false positives in some situations. + * + * @param string $file Pathname to the temporary upload file + * @param string $mime The mime type of the file + * @param string $extension The extension of the file + * @return bool true if the file contains something looking like embedded scripts + */ function detectScript($file, $mime, $extension) { global $wgAllowTitlesInSVG; @@ -1098,93 +1091,103 @@ class UploadForm { return false; } - /** Generic wrapper function for a virus scanner program. - * This relies on the $wgAntivirus and $wgAntivirusSetup variables. - * $wgAntivirusRequired may be used to deny upload if the scan fails. - * - * @param string $file Pathname to the temporary upload file - * @return mixed false if not virus is found, NULL if the scan fails or is disabled, - * or a string containing feedback from the virus scanner if a virus was found. - * If textual feedback is missing but a virus was found, this function returns true. - */ + /** + * Generic wrapper function for a virus scanner program. + * This relies on the $wgAntivirus and $wgAntivirusSetup variables. + * $wgAntivirusRequired may be used to deny upload if the scan fails. + * + * @param string $file Pathname to the temporary upload file + * @return mixed false if not virus is found, NULL if the scan fails or is disabled, + * or a string containing feedback from the virus scanner if a virus was found. + * If textual feedback is missing but a virus was found, this function returns true. + */ function detectVirus($file) { global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; - $fname= "SpecialUpload::detectVirus"; - - if (!$wgAntivirus) { #disabled? - wfDebug("$fname: virus scanner disabled\n"); - + if ( !$wgAntivirus ) { + wfDebug( __METHOD__.": virus scanner disabled\n"); return NULL; } - if (!$wgAntivirusSetup[$wgAntivirus]) { - wfDebug("$fname: unknown virus scanner: $wgAntivirus\n"); - - $wgOut->addHTML( "
    Bad configuration: unknown virus scanner: $wgAntivirus
    \n" ); #LOCALIZE - + if ( !$wgAntivirusSetup[$wgAntivirus] ) { + wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" ); + # @TODO: localise + $wgOut->addHTML( "
    Bad configuration: unknown virus scanner: $wgAntivirus
    \n" ); return "unknown antivirus: $wgAntivirus"; } - #look up scanner configuration - $virus_scanner= $wgAntivirusSetup[$wgAntivirus]["command"]; #command pattern - $virus_scanner_codes= $wgAntivirusSetup[$wgAntivirus]["codemap"]; #exit-code map - $msg_pattern= $wgAntivirusSetup[$wgAntivirus]["messagepattern"]; #message pattern - - $scanner= $virus_scanner; #copy, so we can resolve the pattern + # look up scanner configuration + $command = $wgAntivirusSetup[$wgAntivirus]["command"]; + $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"]; + $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ? + $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null; - if (strpos($scanner,"%f")===false) $scanner.= " ".wfEscapeShellArg($file); #simple pattern: append file to scan - else $scanner= str_replace("%f",wfEscapeShellArg($file),$scanner); #complex pattern: replace "%f" with file to scan + if ( strpos( $command,"%f" ) === false ) { + # simple pattern: append file to scan + $command .= " " . wfEscapeShellArg( $file ); + } else { + # complex pattern: replace "%f" with file to scan + $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); + } - wfDebug("$fname: running virus scan: $scanner \n"); + wfDebug( __METHOD__.": running virus scan: $command \n" ); - #execute virus scanner - $code= false; + # execute virus scanner + $exitCode = false; #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. # that does not seem to be worth the pain. # Ask me (Duesentrieb) about it if it's ever needed. $output = array(); - if (wfIsWindows()) exec("$scanner",$output,$code); - else exec("$scanner 2>&1",$output,$code); - - $exit_code= $code; #remember for user feedback + if ( wfIsWindows() ) { + exec( "$command", $output, $exitCode ); + } else { + exec( "$command 2>&1", $output, $exitCode ); + } - if ($virus_scanner_codes) { #map exit code to AV_xxx constants. - if (isset($virus_scanner_codes[$code])) { - $code= $virus_scanner_codes[$code]; # explicit mapping - } else if (isset($virus_scanner_codes["*"])) { - $code= $virus_scanner_codes["*"]; # fallback mapping + # map exit code to AV_xxx constants. + $mappedCode = $exitCode; + if ( $exitCodeMap ) { + if ( isset( $exitCodeMap[$exitCode] ) ) { + $mappedCode = $exitCodeMap[$exitCode]; + } elseif ( isset( $exitCodeMap["*"] ) ) { + $mappedCode = $exitCodeMap["*"]; } } - if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes) - wfDebug("$fname: failed to scan $file (code $exit_code).\n"); + if ( $mappedCode === AV_SCAN_FAILED ) { + # scan failed (code was mapped to false by $exitCodeMap) + wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); - if ($wgAntivirusRequired) { return "scan failed (code $exit_code)"; } - else { return NULL; } - } - else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune) - wfDebug("$fname: unsupported file type $file (code $exit_code).\n"); + if ( $wgAntivirusRequired ) { + return "scan failed (code $exitCode)"; + } else { + return NULL; + } + } else if ( $mappedCode === AV_SCAN_ABORTED ) { + # scan failed because filetype is unknown (probably imune) + wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" ); return NULL; - } - else if ($code===AV_NO_VIRUS) { - wfDebug("$fname: file passed virus scan.\n"); - return false; #no virus found - } - else { - $output= join("\n",$output); - $output= trim($output); - - if (!$output) $output= true; #if there's no output, return true - else if ($msg_pattern) { - $groups= array(); - if (preg_match($msg_pattern,$output,$groups)) { - if ($groups[1]) $output= $groups[1]; + } else if ( $mappedCode === AV_NO_VIRUS ) { + # no virus found + wfDebug( __METHOD__.": file passed virus scan.\n" ); + return false; + } else { + $output = join( "\n", $output ); + $output = trim( $output ); + + if ( !$output ) { + $output = true; #if there's no output, return true + } elseif ( $msgPattern ) { + $groups = array(); + if ( preg_match( $msgPattern, $output, $groups ) ) { + if ( $groups[1] ) { + $output = $groups[1]; + } } } - wfDebug("$fname: FOUND VIRUS! scanner feedback: $output"); + wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" ); return $output; } } @@ -1198,7 +1201,7 @@ class UploadForm { * @access private */ function checkMacBinary() { - $macbin = new MacBinary( $this->mUploadTempName ); + $macbin = new MacBinary( $this->mTempPath ); if( $macbin->isValid() ) { $dataFile = tempnam( wfTempDir(), "WikiMacBinary" ); $dataHandle = fopen( $dataFile, 'wb' ); @@ -1206,8 +1209,8 @@ class UploadForm { wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" ); $macbin->extractData( $dataHandle ); - $this->mUploadTempName = $dataFile; - $this->mUploadSize = $macbin->dataForkLength(); + $this->mTempPath = $dataFile; + $this->mFileSize = $macbin->dataForkLength(); // We'll have to manually remove the new file if it's not kept. $this->mRemoveTempFile = true; @@ -1221,9 +1224,9 @@ class UploadForm { * @access private */ function cleanupTempFile() { - if( $this->mRemoveTempFile && file_exists( $this->mUploadTempName ) ) { - wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file $this->mUploadTempName\n" ); - unlink( $this->mUploadTempName ); + if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) { + wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" ); + unlink( $this->mTempPath ); } } @@ -1296,5 +1299,30 @@ class UploadForm { $wgOut->enableClientCache( false ); $wgOut->addWikiText( $error->getMessage() ); } + + /** + * Get the initial image page text based on a comment and optional file status information + */ + static function getInitialPageText( $comment, $license, $copyStatus, $source ) { + global $wgUseCopyrightUpload; + if ( $wgUseCopyrightUpload ) { + if ( $license != '' ) { + $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } + $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" . + '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . + "$licensetxt" . + '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; + } else { + if ( $license != '' ) { + $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n"; + $pageText = $filedesc . + '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } else { + $pageText = $comment; + } + } + return $pageText; + } } ?> diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 39684847a2..4cb0a4d895 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -3,85 +3,20 @@ /** * A repository for files accessible via the local filesystem. Does not support * database access or registration. - * - * TODO: split off abstract base FileRepo */ -class FSRepo { - const DELETE_SOURCE = 1; - - var $directory, $url, $hashLevels, $thumbScriptUrl, $transformVia404; - var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; +class FSRepo extends FileRepo { + var $directory, $url, $hashLevels; var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); var $oldFileFactory = false; function __construct( $info ) { + parent::__construct( $info ); + // Required settings - $this->name = $info['name']; $this->directory = $info['directory']; $this->url = $info['url']; $this->hashLevels = $info['hashLevels']; - $this->transformVia404 = !empty( $info['transformVia404'] ); - - // Optional settings - $this->initialCapital = true; // by default - foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', - 'thumbScriptUrl', 'initialCapital' ) as $var ) - { - if ( isset( $info[$var] ) ) { - $this->$var = $info[$var]; - } - } - } - - /** - * Create a new File object from the local repository - * @param mixed $title Title object or string - * @param mixed $time Time at which the image is supposed to have existed. - * If this is specified, the returned object will be an - * instance of the repository's old file class instead of - * a current file. Repositories not supporting version - * control should return false if this parameter is set. - */ - function newFile( $title, $time = false ) { - if ( !($title instanceof Title) ) { - $title = Title::makeTitleSafe( NS_IMAGE, $title ); - if ( !is_object( $title ) ) { - return null; - } - } - if ( $time ) { - if ( $this->oldFileFactory ) { - return call_user_func( $this->oldFileFactory, $title, $this, $time ); - } else { - return false; - } - } else { - return call_user_func( $this->fileFactory, $title, $this ); - } - } - - /** - * Find an instance of the named file that existed at the specified time - * Returns false if the file did not exist. Repositories not supporting - * version control should return false if the time is specified. - * - * @param mixed $time 14-character timestamp, or false for the current version - */ - function findFile( $title, $time = false ) { - # First try the current version of the file to see if it precedes the timestamp - $img = $this->newFile( $title ); - if ( !$img ) { - return false; - } - if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) { - return $img; - } - # Now try an old version of the file - $img = $this->newFile( $title, $time ); - if ( $img->exists() ) { - return $img; - } } /** @@ -105,20 +40,6 @@ class FSRepo { return (bool)$this->hashLevels; } - /** - * Get the URL of thumb.php - */ - function getThumbScriptUrl() { - return $this->thumbScriptUrl; - } - - /** - * Returns true if the repository can transform files via a 404 handler - */ - function canTransformVia404() { - return $this->transformVia404; - } - /** * Get the local directory corresponding to one of the three basic zones */ @@ -153,11 +74,13 @@ class FSRepo { /** * Get a URL referring to this repository, with the private mwrepo protocol. + * The suffix, if supplied, is considered to be unencoded, and will be + * URL-encoded before being returned. */ function getVirtualUrl( $suffix = false ) { - $path = 'mwrepo://'; + $path = 'mwrepo://' . $this->name; if ( $suffix !== false ) { - $path .= '/' . $suffix; + $path .= '/' . rawurlencode( $suffix ); } return $path; } @@ -174,21 +97,24 @@ class FSRepo { if ( count( $bits ) != 3 ) { throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); } - list( $host, $zone, $rel ) = $bits; - if ( $host !== '' ) { + list( $repo, $zone, $rel ) = $bits; + if ( $repo !== $this->name ) { throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); } $base = $this->getZonePath( $zone ); if ( !$base ) { throw new MWException( __METHOD__.": invalid zone: $zone" ); } - return $base . '/' . urldecode( $rel ); + return $base . '/' . rawurldecode( $rel ); } /** * Store a file to a given destination. */ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + if ( !is_writable( $this->directory ) ) { + return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) ); + } $root = $this->getZonePath( $dstZone ); if ( !$root ) { throw new MWException( "Invalid zone: $dstZone" ); @@ -198,8 +124,8 @@ class FSRepo { if ( !is_dir( dirname( $dstPath ) ) ) { wfMkdirParents( dirname( $dstPath ) ); } - - if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { + + if ( self::isVirtualUrl( $srcPath ) ) { $srcPath = $this->resolveVirtualUrl( $srcPath ); } @@ -245,7 +171,7 @@ class FSRepo { * @return boolean True on success, false on failure */ function freeTemp( $virtualUrl ) { - $temp = 'mwrepo:///temp'; + $temp = "mwrepo://{$this->name}/temp"; if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { wfDebug( __METHOD__.": Invalid virtual URL\n" ); return false; @@ -263,16 +189,28 @@ class FSRepo { * virtual URL, into this repository at the specified destination location. * * @param string $srcPath The source path or URL - * @param string $dstPath The destination relative path - * @param string $archivePath The relative path where the existing file is to - * be archived, if there is one. - * @param integer $flags Bitfield, may be FSRepo::DELETE_SOURCE to indicate + * @param string $dstRel The destination relative path + * @param string $archiveRel The relative path where the existing file is to + * be archived, if there is one. Relative to the public zone root. + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source file should be deleted if possible */ - function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) { + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { + if ( !is_writable( $this->directory ) ) { + return new WikiErrorMsg( 'upload_directory_read_only', wfEscapeWikiText( $this->directory ) ); + } if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { $srcPath = $this->resolveVirtualUrl( $srcPath ); } + if ( !$this->validateFilename( $dstRel ) ) { + throw new MWException( 'Validation error in $dstRel' ); + } + if ( !$this->validateFilename( $archiveRel ) ) { + throw new MWException( 'Validation error in $archiveRel' ); + } + $dstPath = "{$this->directory}/$dstRel"; + $archivePath = "{$this->directory}/$archiveRel"; + $dstDir = dirname( $dstPath ); if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir ); @@ -324,81 +262,7 @@ class FSRepo { * If the repo is not hashed, returns an empty string */ function getHashPath( $name ) { - if ( $this->isHashed() ) { - $hash = md5( $name ); - $path = ''; - for ( $i = 1; $i <= $this->hashLevels; $i++ ) { - $path .= substr( $hash, 0, $i ) . '/'; - } - return $path; - } else { - return ''; - } - } - - /** - * Get the name of this repository, as specified by $info['name]' to the constructor - */ - function getName() { - return $this->name; - } - - /** - * Get the file description page base URL, or false if there isn't one. - * @private - */ - function getDescBaseUrl() { - if ( is_null( $this->descBaseUrl ) ) { - if ( !is_null( $this->articleUrl ) ) { - $this->descBaseUrl = str_replace( '$1', - urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); - } elseif ( !is_null( $this->scriptDirUrl ) ) { - $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . - urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':'; - } else { - $this->descBaseUrl = false; - } - } - return $this->descBaseUrl; - } - - /** - * Get the URL of an image description page. May return false if it is - * unknown or not applicable. In general this should only be called by the - * File class, since it may return invalid results for certain kinds of - * repositories. Use File::getDescriptionUrl() in user code. - * - * In particular, it uses the article paths as specified to the repository - * constructor, whereas local repositories use the local Title functions. - */ - function getDescriptionUrl( $name ) { - $base = $this->getDescBaseUrl(); - if ( $base ) { - return $base . wfUrlencode( $name ); - } else { - return false; - } - } - - /** - * Get the URL of the content-only fragment of the description page. For - * MediaWiki this means action=render. This should only be called by the - * repository's file class, since it may return invalid results. User code - * should use File::getDescriptionText(). - */ - function getDescriptionRenderUrl( $name ) { - if ( isset( $this->scriptDirUrl ) ) { - return $this->scriptDirUrl . '/index.php?title=' . - wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . - '&action=render'; - } else { - $descBase = $this->getDescBaseUrl(); - if ( $descBase ) { - return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' ); - } else { - return false; - } - } + return FileRepo::getHashPathForLevel( $name, $this->hashLevels ); } /** @@ -424,7 +288,7 @@ class FSRepo { } /** - * Call a callaback function for every file in the repository + * Call a callback function for every file in the repository * May use either the database or the filesystem */ function enumFiles( $callback ) { @@ -432,20 +296,12 @@ class FSRepo { } /** - * Get the name of an image from its title object + * Get properties of a file with a given virtual URL + * The virtual URL must refer to this repo */ - function getNameFromTitle( $title ) { - global $wgCapitalLinks; - if ( $this->initialCapital != $wgCapitalLinks ) { - global $wgContLang; - $name = $title->getUserCaseDBKey(); - if ( $this->initialCapital ) { - $name = $wgContLang->ucfirst( $name ); - } - } else { - $name = $title->getDBkey(); - } - return $name; + function getFileProps( $virtualUrl ) { + $path = $this->resolveVirtualUrl( $virtualUrl ); + return File::getPropsFromPath( $path ); } } diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 35ce4c96c9..4325f791c2 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -503,7 +503,7 @@ class File { break; } - wfDebug( "Doing stat for $thumbPath\n" ); + wfDebug( __METHOD__.": Doing stat for $thumbPath\n" ); $this->migrateThumbFile( $thumbName ); if ( file_exists( $thumbPath ) ) { $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); @@ -663,7 +663,7 @@ class File { * Get urlencoded relative path of the file */ function getUrlRel() { - return $this->getHashPath() . urlencode( $this->getName() ); + return $this->getHashPath() . rawurlencode( $this->getName() ); } /** Get the path of the archive directory, or a particular file if $suffix is specified */ @@ -692,7 +692,7 @@ class File { if ( $suffix === false ) { $path = substr( $path, 0, -1 ); } else { - $path .= urlencode( $suffix ); + $path .= rawurlencode( $suffix ); } return $path; } @@ -701,7 +701,7 @@ class File { function getThumbUrl( $suffix = false ) { $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel(); if ( $suffix !== false ) { - $path .= '/' . urlencode( $suffix ); + $path .= '/' . rawurlencode( $suffix ); } return $path; } @@ -712,7 +712,7 @@ class File { if ( $suffix === false ) { $path = substr( $path, 0, -1 ); } else { - $path .= urlencode( $suffix ); + $path .= rawurlencode( $suffix ); } return $path; } @@ -721,11 +721,20 @@ class File { function getThumbVirtualUrl( $suffix = false ) { $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel(); if ( $suffix !== false ) { - $path .= '/' . urlencode( $suffix ); + $path .= '/' . rawurlencode( $suffix ); } return $path; } + /** Get the virtual URL for the file itself */ + function getVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel(); + if ( $suffix !== false ) { + $path .= '/' . rawurlencode( $suffix ); + } + return $path; + } + /** * @return bool */ @@ -990,6 +999,59 @@ class File { function userCan( $field ) { return true; } + + /** + * Get an associative array containing information about a file in the local filesystem + */ + static function getPropsFromPath( $path ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__.": Getting file info for $path\n" ); + $info = array( 'fileExists' => file_exists( $path ) ); + $gis = false; + if ( $info['fileExists'] ) { + $magic = MimeMagic::singleton(); + + $info['mime'] = $magic->guessMimeType( $path, true ); + list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] ); + $info['media_type'] = $magic->getMediaType( $path, $info['mime'] ); + + # Get size in bytes + $info['size'] = filesize( $path ); + + # Height, width and metadata + $handler = MediaHandler::getHandler( $info['mime'] ); + if ( $handler ) { + $tempImage = (object)array(); + $gis = $handler->getImageSize( $tempImage, $path ); + $info['metadata'] = $handler->getMetadata( $tempImage, $path ); + } else { + $gis = false; + $info['metadata'] = ''; + } + + wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n"); + } else { + $info['mime'] = NULL; + $info['media_type'] = MEDIATYPE_UNKNOWN; + $info['metadata'] = ''; + wfDebug(__METHOD__.": $path NOT FOUND!\n"); + } + if( $gis ) { + # NOTE: $gis[2] contains a code for the image type. This is no longer used. + $info['width'] = $gis[0]; + $info['height'] = $gis[1]; + if ( isset( $gis['bits'] ) ) { + $info['bits'] = $gis['bits']; + } else { + $info['bits'] = 0; + } + } else { + $info['width'] = 0; + $info['height'] = 0; + } + wfProfileOut( __METHOD__ ); + return $info; + } } ?> diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php new file mode 100644 index 0000000000..84f7a57296 --- /dev/null +++ b/includes/filerepo/FileRepo.php @@ -0,0 +1,280 @@ +name = $info['name']; + + // Optional settings + $this->initialCapital = true; // by default + foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', + 'thumbScriptUrl', 'initialCapital' ) as $var ) + { + if ( isset( $info[$var] ) ) { + $this->$var = $info[$var]; + } + } + $this->transformVia404 = !empty( $info['transformVia404'] ); + } + + /** + * Determine if a string is an mwrepo:// URL + */ + static function isVirtualUrl( $url ) { + return substr( $url, 0, 9 ) == 'mwrepo://'; + } + + /** + * Create a new File object from the local repository + * @param mixed $title Title object or string + * @param mixed $time Time at which the image is supposed to have existed. + * If this is specified, the returned object will be an + * instance of the repository's old file class instead of + * a current file. Repositories not supporting version + * control should return false if this parameter is set. + */ + function newFile( $title, $time = false ) { + if ( !($title instanceof Title) ) { + $title = Title::makeTitleSafe( NS_IMAGE, $title ); + if ( !is_object( $title ) ) { + return null; + } + } + if ( $time ) { + if ( $this->oldFileFactory ) { + return call_user_func( $this->oldFileFactory, $title, $this, $time ); + } else { + return false; + } + } else { + return call_user_func( $this->fileFactory, $title, $this ); + } + } + + /** + * Find an instance of the named file that existed at the specified time + * Returns false if the file did not exist. Repositories not supporting + * version control should return false if the time is specified. + * + * @param mixed $time 14-character timestamp, or false for the current version + */ + function findFile( $title, $time = false ) { + # First try the current version of the file to see if it precedes the timestamp + $img = $this->newFile( $title ); + if ( !$img ) { + return false; + } + if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) { + return $img; + } + # Now try an old version of the file + $img = $this->newFile( $title, $time ); + if ( $img->exists() ) { + return $img; + } + } + + /** + * Get the URL of thumb.php + */ + function getThumbScriptUrl() { + return $this->thumbScriptUrl; + } + + /** + * Returns true if the repository can transform files via a 404 handler + */ + function canTransformVia404() { + return $this->transformVia404; + } + + /** + * Get the name of an image from its title object + */ + function getNameFromTitle( $title ) { + global $wgCapitalLinks; + if ( $this->initialCapital != $wgCapitalLinks ) { + global $wgContLang; + $name = $title->getUserCaseDBKey(); + if ( $this->initialCapital ) { + $name = $wgContLang->ucfirst( $name ); + } + } else { + $name = $title->getDBkey(); + } + return $name; + } + + static function getHashPathForLevel( $name, $levels ) { + if ( $levels == 0 ) { + return ''; + } else { + $hash = md5( $name ); + $path = ''; + for ( $i = 1; $i <= $levels; $i++ ) { + $path .= substr( $hash, 0, $i ) . '/'; + } + return $path; + } + } + + /** + * Get the name of this repository, as specified by $info['name]' to the constructor + */ + function getName() { + return $this->name; + } + + /** + * Get the file description page base URL, or false if there isn't one. + * @private + */ + function getDescBaseUrl() { + if ( is_null( $this->descBaseUrl ) ) { + if ( !is_null( $this->articleUrl ) ) { + $this->descBaseUrl = str_replace( '$1', + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); + } elseif ( !is_null( $this->scriptDirUrl ) ) { + $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':'; + } else { + $this->descBaseUrl = false; + } + } + return $this->descBaseUrl; + } + + /** + * Get the URL of an image description page. May return false if it is + * unknown or not applicable. In general this should only be called by the + * File class, since it may return invalid results for certain kinds of + * repositories. Use File::getDescriptionUrl() in user code. + * + * In particular, it uses the article paths as specified to the repository + * constructor, whereas local repositories use the local Title functions. + */ + function getDescriptionUrl( $name ) { + $base = $this->getDescBaseUrl(); + if ( $base ) { + return $base . wfUrlencode( $name ); + } else { + return false; + } + } + + /** + * Get the URL of the content-only fragment of the description page. For + * MediaWiki this means action=render. This should only be called by the + * repository's file class, since it may return invalid results. User code + * should use File::getDescriptionText(). + */ + function getDescriptionRenderUrl( $name ) { + if ( isset( $this->scriptDirUrl ) ) { + return $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . + '&action=render'; + } else { + $descBase = $this->getDescBaseUrl(); + if ( $descBase ) { + return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' ); + } else { + return false; + } + } + } + + /** + * Store a file to a given destination. + */ + abstract function store( $srcPath, $dstZone, $dstRel, $flags = 0 ); + + /** + * Pick a random name in the temp zone and store a file to it. + * Returns the URL, or a WikiError on failure. + * @param string $originalName The base name of the file as specified + * by the user. The file extension will be maintained. + * @param string $srcPath The current location of the file. + */ + abstract function storeTemp( $originalName, $srcPath ); + + /** + * Remove a temporary file or mark it for garbage collection + * @param string $virtualUrl The virtual URL returned by storeTemp + * @return boolean True on success, false on failure + * STUB + */ + function freeTemp( $virtualUrl ) { + return true; + } + + /** + * Copy or move a file either from the local filesystem or from an mwrepo:// + * virtual URL, into this repository at the specified destination location. + * + * @param string $srcPath The source path or URL + * @param string $dstRel The destination relative path + * @param string $archiveRel The relative path where the existing file is to + * be archived, if there is one. Relative to the public zone root. + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source file should be deleted if possible + */ + abstract function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ); + + /** + * Get properties of a file with a given virtual URL + * The virtual URL must refer to this repo + * Properties should ultimately be obtained via File::getPropsFromPath() + */ + abstract function getFileProps( $virtualUrl ); + + /** + * Call a callback function for every file in the repository + * May use either the database or the filesystem + * STUB + */ + function enumFiles( $callback ) { + throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) ); + } + + /** + * Determine if a relative path is valid, i.e. not blank or involving directory traveral + */ + function validateFilename( $filename ) { + if ( strval( $filename ) == '' ) { + return false; + } + if ( wfIsWindows() ) { + $filename = strtr( $filename, '\\', '/' ); + } + /** + * Use the same traversal protection as Title::secureAndSplit() + */ + if ( strpos( $filename, '.' ) !== false && + ( $filename === '.' || $filename === '..' || + strpos( $filename, './' ) === 0 || + strpos( $filename, '../' ) === 0 || + strpos( $filename, '/./' ) !== false || + strpos( $filename, '/../' ) !== false ) ) + { + return false; + } else { + return true; + } + } +} +?> diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index fc3c829cbd..1a93d5e88c 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -43,7 +43,7 @@ class ForeignDBRepo extends LocalRepo { return $this->hasSharedCache; } - function store( /*...*/ ) { + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { throw new MWException( get_class($this) . ': write operations are not supported' ); } } diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 8b8486b3b0..2e1f8a8b66 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -150,58 +150,7 @@ class LocalFile extends File * Load metadata from the file itself */ function loadFromFile() { - wfProfileIn( __METHOD__ ); - $path = $this->getPath(); - $this->fileExists = file_exists( $path ); - $gis = array(); - - if ( $this->fileExists ) { - $magic=& MimeMagic::singleton(); - - $this->mime = $magic->guessMimeType($path,true); - list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime ); - $this->media_type = $magic->getMediaType($path,$this->mime); - $handler = MediaHandler::getHandler( $this->mime ); - - # Get size in bytes - $this->size = filesize( $path ); - - # Height, width and metadata - if ( $handler ) { - $gis = $handler->getImageSize( $this, $path ); - $this->metadata = $handler->getMetadata( $this, $path ); - } else { - $gis = false; - $this->metadata = ''; - } - - wfDebug(__METHOD__.": $path loaded, {$this->size} bytes, {$this->mime}.\n"); - } else { - $this->mime = NULL; - $this->media_type = MEDIATYPE_UNKNOWN; - $this->metadata = ''; - wfDebug(__METHOD__.": $path NOT FOUND!\n"); - } - - if( $gis ) { - $this->width = $gis[0]; - $this->height = $gis[1]; - } else { - $this->width = 0; - $this->height = 0; - } - - #NOTE: $gis[2] contains a code for the image type. This is no longer used. - - #NOTE: we have to set this flag early to avoid load() to be called - # be some of the functions below. This may lead to recursion or other bad things! - # as ther's only one thread of execution, this should be safe anyway. - $this->dataLoaded = true; - - if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; - else $this->bits = 0; - - wfProfileOut( __METHOD__ ); + $this->setProps( self::getInfoFromPath( $this->getPath() ) ); } function getCacheFields( $prefix = 'img_' ) { @@ -349,6 +298,17 @@ class LocalFile extends File wfProfileOut( __METHOD__ ); } + function setProps( $info ) { + $this->dataLoaded = true; + $fields = $this->getCacheFields( '' ); + $fields[] = 'fileExists'; + foreach ( $fields as $field ) { + if ( isset( $info[$field] ) ) { + $this->$field = $info[$field]; + } + } + } + /** splitMime inherited */ /** getName inherited */ /** getTitle inherited */ @@ -517,20 +477,29 @@ class LocalFile extends File * Refresh metadata in memcached, but don't touch thumbnails or squid */ function purgeMetadataCache() { - clearstatcache(); - $this->loadFromFile(); + $this->loadFromDB(); $this->saveToCache(); } /** * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid */ - function purgeCache( $archiveFiles = array() ) { - global $wgUseSquid; - + function purgeCache() { // Refresh metadata cache $this->purgeMetadataCache(); + // Delete thumbnails + $this->purgeThumbnails(); + + // Purge squid cache for this file + wfPurgeSquidServers( array( $this->getURL() ) ); + } + + /** + * Delete cached transformed files + */ + function purgeThumbnails() { + global $wgUseSquid; // Delete thumbnails $files = $this->getThumbnails(); $dir = $this->getThumbPath(); @@ -548,10 +517,6 @@ class LocalFile extends File // Purge the squid if ( $wgUseSquid ) { - $urls[] = $this->getURL(); - foreach ( $archiveFiles as $file ) { - $urls[] = $this->getArchiveUrl( $file ); - } wfPurgeSquidServers( $urls ); } } @@ -631,18 +596,67 @@ class LocalFile extends File /** getThumbVirtualUrl inherited */ /** isHashed inherited */ + /** + * Upload a file and record it in the DB + * @param string $srcPath Source path or virtual URL + * @param string $comment Upload description + * @param string $pageText Text to use for the new description page, if a new description page is created + * @param integer $flags Flags for publish() + * @param array $props File properties, if known. This can be used to reduce the + * upload time when uploading virtual URLs for which the file info + * is already known + * @param string $timestamp Timestamp for img_timestamp, or false to use the current time + * + * @return Wikitext-formatted WikiError or true on success + */ + function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) { + $archive = $this->publish( $srcPath, $flags ); + if ( WikiError::isError( $archive ) ){ + return $archive; + } + if ( !$this->recordUpload2( $archive, $comment, $pageText, $props, $timestamp ) ) { + return new WikiErrorMsg( 'filenotfound', wfEscapeWikiText( $srcPath ) ); + } + return true; + } + /** * Record a file upload in the upload log and the image table + * @deprecated use upload() */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false ) { - global $wgUser, $wgUseCopyrightUpload; + $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source ); + if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) { + return false; + } + if ( $watch ) { + global $wgUser; + $wgUser->addWatch( $this->getTitle() ); + } + return true; + + } + + /** + * Record a file upload in the upload log and the image table + */ + function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false ) + { + global $wgUser; $dbw = $this->repo->getMasterDB(); + if ( !$props ) { + $props = $this->repo->getFileProps( $this->getVirtualUrl() ); + } + $this->setProps( $props ); + // Delete thumbnails and refresh the metadata cache - $this->purgeCache(); + $this->purgeThumbnails(); + $this->saveToCache(); + wfPurgeSquidServers( array( $this->getURL() ) ); // Fail now if the file isn't there if ( !$this->fileExists ) { @@ -650,37 +664,10 @@ class LocalFile extends File return false; } - if ( $wgUseCopyrightUpload ) { - if ( $license != '' ) { - $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } - $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" . - '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . - "$licensetxt" . - '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; - } else { - if ( $license != '' ) { - $filedesc = $desc == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n"; - $textdesc = $filedesc . - '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } else { - $textdesc = $desc; - } - } - if ( $timestamp === false ) { $timestamp = $dbw->timestamp(); } - #split mime type - if (strpos($this->mime,'/')!==false) { - list($major,$minor)= explode('/',$this->mime,2); - } - else { - $major= $this->mime; - $minor= "unknown"; - } - # Test to see if the row exists using INSERT IGNORE # This avoids race conditions by locking the row until the commit, and also # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. @@ -692,10 +679,10 @@ class LocalFile extends File 'img_height' => intval( $this->height ), 'img_bits' => $this->bits, 'img_media_type' => $this->media_type, - 'img_major_mime' => $major, - 'img_minor_mime' => $minor, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, - 'img_description' => $desc, + 'img_description' => $comment, 'img_user' => $wgUser->getID(), 'img_user_text' => $wgUser->getName(), 'img_metadata' => $this->metadata, @@ -730,10 +717,10 @@ class LocalFile extends File 'img_height' => intval( $this->height ), 'img_bits' => $this->bits, 'img_media_type' => $this->media_type, - 'img_major_mime' => $major, - 'img_minor_mime' => $minor, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, - 'img_description' => $desc, + 'img_description' => $comment, 'img_user' => $wgUser->getID(), 'img_user_text' => $wgUser->getName(), 'img_metadata' => $this->metadata, @@ -750,31 +737,28 @@ class LocalFile extends File $descTitle = $this->getTitle(); $article = new Article( $descTitle ); - $minor = false; - $watch = $watch || $wgUser->isWatched( $descTitle ); - $suppressRC = true; // There's already a log entry, so don't double the RC load + + # Add the log entry + $log = new LogPage( 'upload' ); + $log->addEntry( 'upload', $descTitle, $comment ); if( $descTitle->exists() ) { - // TODO: insert a null revision into the page history for this update. - if( $watch ) { - $wgUser->addWatch( $descTitle ); - } + # Create a null revision + $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false ); + $nullRevision->insertOn( $dbw ); # Invalidate the cache for the description page $descTitle->invalidateCache(); $descTitle->purgeSquid(); } else { // New file; create the description page. - $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC ); + // There's already a log entry, so don't make a second RC entry + $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC ); } # Hooks, hooks, the magic of hooks... wfRunHooks( 'FileUpload', array( $this ) ); - # Add the log entry - $log = new LogPage( 'upload' ); - $log->addEntry( 'upload', $descTitle, $desc ); - # Commit the transaction now, in case something goes wrong later # The most important thing is that files don't get lost, especially archives $dbw->immediateCommit(); @@ -803,11 +787,11 @@ class LocalFile extends File * file, and a wikitext-formatted WikiError object on failure. */ function publish( $srcPath, $flags = 0 ) { - $dstPath = $this->getFullPath(); + $dstRel = $this->getRel(); $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); - $archivePath = $this->getArchivePath( $archiveName ); + $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; - $status = $this->repo->publish( $srcPath, $dstPath, $archivePath, $flags ); + $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags ); if ( WikiError::isError( $status ) ) { return $status; } elseif ( $status == 'new' ) { diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 1c35eaa5a0..70dd6d87fd 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -116,6 +116,35 @@ class RepoGroup { $class = $info['class']; return new $class( $info ); } + + /** + * Split a virtual URL into repo, zone and rel parts + * @return an array containing repo, zone and rel + */ + function splitVirtualUrl( $url ) { + if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { + throw new MWException( __METHOD__.': unknown protoocl' ); + } + + $bits = explode( '/', substr( $url, 9 ), 3 ); + if ( count( $bits ) != 3 ) { + throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); + } + return $bits; + } + + function getFileProps( $fileName ) { + if ( FileRepo::isVirtualUrl( $fileName ) ) { + list( $repoName, $zone, $rel ) = $this->splitVirtualUrl( $fileName ); + if ( $repoName === '' ) { + $repoName = 'local'; + } + $repo = $this->getRepo( $repoName ); + return $repo->getFileProps( $fileName ); + } else { + return File::getPropsFromPath( $fileName ); + } + } } ?> diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php index ff9c5df08d..ca4dbc4c7c 100644 --- a/includes/filerepo/UnregisteredLocalFile.php +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -92,7 +92,7 @@ class UnregisteredLocalFile extends File { function getURL() { if ( $this->repo ) { - return $this->repo->getZoneUrl( 'public' ) . $this->repo->getHashPath( $this->name ) . urlencode( $this->name ); + return $this->repo->getZoneUrl( 'public' ) . '/' . $this->repo->getHashPath( $this->name ) . urlencode( $this->name ); } else { return false; } diff --git a/tests/LocalFileTest.php b/tests/LocalFileTest.php index 7f0853bf29..830ad5a394 100644 --- a/tests/LocalFileTest.php +++ b/tests/LocalFileTest.php @@ -68,17 +68,17 @@ class LocalFileTest extends PHPUnit_Framework_TestCase { } function testGetArchiveVirtualUrl() { - $this->assertEquals( 'mwrepo:///public/archive', $this->file_hl0->getArchiveVirtualUrl() ); - $this->assertEquals( 'mwrepo:///public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() ); - $this->assertEquals( 'mwrepo:///public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) ); - $this->assertEquals( 'mwrepo:///public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) ); } function testGetThumbVirtualUrl() { - $this->assertEquals( 'mwrepo:///public/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() ); - $this->assertEquals( 'mwrepo:///public/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() ); - $this->assertEquals( 'mwrepo:///public/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) ); - $this->assertEquals( 'mwrepo:///public/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) ); } function testGetUrl() { -- 2.20.1
    - +
    - + {$this->uploadFormTextAfterSummary}