Robert Vogel has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/326915 )
Change subject: Fixing for REL1_27 ...................................................................... Fixing for REL1_27 As https://gerrit.wikimedia.org/r/#/c/326127/ was a dead end, this is the new approach to fix the extension for REL1_27 Change-Id: I38843ba186bbb3e620c0b2e7e252dbdf9827f477 --- M extension.json M includes/DefaultSettings.php M includes/NSFileRepoHooks.php M includes/filebackend/NSFileRepoFSFileBackend.php M includes/filerepo/file/NSLocalFile.php A includes/utility/NSFileRepoHelper.php A nsfr_img_auth.php 7 files changed, 341 insertions(+), 107 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/NSFileRepo refs/changes/15/326915/1 diff --git a/extension.json b/extension.json index fd0fe0b..b7e5f46 100644 --- a/extension.json +++ b/extension.json @@ -27,7 +27,8 @@ "NSLocalFile": "includes/filerepo/file/NSLocalFile.php", "NSLocalFileMoveBatch": "includes/filerepo/file/NSLocalFile.php", "NSOldLocalFile": "includes/filerepo/file/NSOldLocalFile.php", - "NSFileRepoFSFileBackend": "includes/filebackend/NSFileRepoFSFileBackend.php" + "NSFileRepoFSFileBackend": "includes/filebackend/NSFileRepoFSFileBackend.php", + "NSFileRepoHelper": "includes/utility/NSFileRepoHelper.php" }, "ResourceModules": { "ext.nsfilerepo.special.upload": { @@ -45,7 +46,10 @@ "@note": "Note, this must be AFTER Extension:Lockdown has been included - thus assuming that the user has access to files in general + files at this particular namespace.", "userCan": "NSFileRepoHooks::onUserCan", "BeforePageDisplay": "NSFileRepoHooks::onBeforePageDisplay", - "UploadFormInitDescriptor": "NSFileRepoHooks::onUploadFormInitDescriptor" + "UploadFormInitDescriptor": "NSFileRepoHooks::onUploadFormInitDescriptor", + "ImgAuthBeforeCheckFileExists": "NSFileRepoHooks::onImgAuthBeforeCheckFileExists", + "ImgAuthBeforeStream": "NSFileRepoHooks::onImgAuthBeforeStream", + "UploadVerification": "NSFileRepoHooks::onUploadVerification" }, "manifest_version": 1 } \ No newline at end of file diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 79041e9..2d43366 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -5,14 +5,8 @@ */ // Remove the default illegal char ':' - needed it to determine NS -$GLOBALS['wgIllegalFileChars'] = str_replace( ":","",$GLOBALS['wgIllegalFileChars'] ); +$GLOBALS['wgIllegalFileChars'] = str_replace( ":", "", $GLOBALS['wgIllegalFileChars'] ); -//Activate img_auth.php -$GLOBALS['wgUploadPath'] = $GLOBALS['wgScriptPath'] .'/img_auth.php'; -$GLOBALS['wgImgAuthUrlPathMap']['/nsfilerepo/'] = 'mwstore://nsfilerepo-fs/namespace/'; -$GLOBALS['wgFileBackends'][] = array( - 'name' => 'nsfilerepo-fs', - 'class' => 'NSFileRepoFSFileBackend', - 'lockManager' => 'fsLockManager', - #'wikiId' => wfWikiID() -); \ No newline at end of file +//Activate "nsfr_img_auth.php" +//This may be obsolete in future MW versions +$GLOBALS['wgUploadPath'] = $GLOBALS['wgScriptPath'] .'/nsfr_img_auth.php'; \ No newline at end of file diff --git a/includes/NSFileRepoHooks.php b/includes/NSFileRepoHooks.php index 1702be0..86a4ed7 100644 --- a/includes/NSFileRepoHooks.php +++ b/includes/NSFileRepoHooks.php @@ -12,9 +12,6 @@ } $GLOBALS['wgLocalFileRepo']['class'] = "NSLocalRepo"; - $GLOBALS['wgLocalFileRepo']['backend'] = "nsfilerepo-fs"; - $GLOBALS['wgLocalFileRepo']['url'] = $GLOBALS['wgScriptPath'] .'/img_auth.php'; - RepoGroup::destroySingleton(); } @@ -37,12 +34,47 @@ } /** + * @param $path + * @param $name + * @param $filename + * @return bool + */ + public static function onImgAuthBeforeCheckFileExists( &$path, &$name, &$filename ) { + $nsfrhelper = new NSFileRepoHelper(); + $title = $nsfrhelper->getTitleFromPath( $path ); + if( $title instanceof Title ) { + $name = $title->getPrefixedDBkey(); + } + + return true; + } + + /** + * @param Title $title + * @param $path + * @param $name + * @param $result + * @return bool + */ + public static function onImgAuthBeforeStream( &$title, &$path, &$name, &$result ) { + $nsfrhelper = new NSFileRepoHelper(); + $title = $nsfrhelper->getTitleFromPath( $path ); + + if( $title instanceof Title === false ) { + $result = array('img-auth-accessdenied', 'img-auth-badtitle', $name); + return false; + } + + return true; + } + + /** * Check for Namespace in Title line * @param UploadForm $uploadForm * @return boolean */ public static function onUploadFormBeforeProcessing( &$uploadForm ) { - $title = Title::newFromText($uploadForm->mDesiredDestName); + $title = Title::newFromText( $uploadForm->mDesiredDestName ); if( $title === null ) { return true; } @@ -57,7 +89,7 @@ } /** - * If Extension:Lockdown has been activated (recommend), check individual namespace protection + * Check individual namespace protection using Extension:Lockdown * @global array $wgWhitelistRead * @param Title $title * @param user $user @@ -69,16 +101,23 @@ global $wgWhitelistRead; if ( $wgWhitelistRead !== false && in_array( $title->getPrefixedText(), $wgWhitelistRead ) ) { return true; - } elseif( function_exists( 'lockdownUserPermissionsErrors' ) ) { - if( $title->getNamespace() == NS_FILE ) { - $ntitle = Title::newFromText( $title->mDbkeyform ); - $ret_val = ( $ntitle->getNamespace() < 100 ) ? - true : lockdownUserPermissionsErrors( $ntitle, $user, $action, $result ); - $result = null; - return $ret_val; - } } - return true; + + if( $title->getNamespace() !== NS_FILE ) { + return true; + } + + $ntitle = Title::newFromText( $title->getDBkey() ); + $ret_val = true; + + //Additional check for NS_MAIN: If a user is not allowed to read NS_MAIN he should also be not allowed + //to view files with no namespace-prefix as they are logically assigned to namespace NS_MAIN + if( $ntitle->getNamespace() < 100 || $ntitle->getNamespace() === NS_MAIN ) { + $ret_val = lockdownUserPermissionsErrors( $ntitle, $user, $action, $result ); + } + + $result = null; + return $ret_val; } /** @@ -187,4 +226,20 @@ } return $aReturn; } + + /** + * Checks if the destination file name contains a valid namespace prefix + * @param string $destName + * @param string $tempPath + * @param string $error + * @return bool + */ + public static function onUploadVerification( $destName, $tempPath, &$error ) { + $title = Title::newFromText( $destName ); + if( strpos( $title->getText(), ':' ) !== false ) { //There is a colon in the name but it was not a valid namespace prefix! + $error = 'illegal-filename'; + return false; + } + return true; + } } \ No newline at end of file diff --git a/includes/filebackend/NSFileRepoFSFileBackend.php b/includes/filebackend/NSFileRepoFSFileBackend.php index ed2e0f9..0cf7ad9 100644 --- a/includes/filebackend/NSFileRepoFSFileBackend.php +++ b/includes/filebackend/NSFileRepoFSFileBackend.php @@ -2,88 +2,13 @@ class NSFileRepoFSFileBackend extends FSFileBackend { - protected $zoneSuffixes = [ - 'public', 'thumb', 'transcoded', 'deleted', 'archive', 'temp' - ]; - /** - * This is a pretty bad workaround for "img_auth.php" and - * "FileRepo/FileBackend" not being able to do proper permission checks in - * REL1_27 of MediaWiki - * For details see: https://phabricator.wikimedia.org/T140334 - * @param array $params - * @return boolean - */ - protected function doGetFileStat( array $params ) { - $unprefixedPath = $this->stripStorageBasePath( $params['src'] ); - //e.g. $unprefixedPath = "5000/b/be/Some_image.png" - // or $unprefixedPath = "thumb/5000/b/be/Some_image.png/300px-Some_image.png" - // or $unprefixedPath = "deleted/5000/b/be/Some_image.png" - // or $unprefixedPath = "archive/5000/b/be/Some_image.png" - // or $unprefixedPath = "temp/5000/b/be/Some_image.png" - // or $unprefixedPath = "4/03/Some_image_that_is_not_in_a_namespace.png" - - $parts = explode( '/', $unprefixedPath ); - if( count($parts) < 4 ) { - return parent::doGetFileStat($params); - } - - if( in_array( $parts[0], $this->zoneSuffixes ) ) { - array_shift( $parts ); - } - - $namespaceId = intval( array_shift( $parts ) ); // = 5000 - $fileName = array_pop( $parts ); // = "Some_image.png" or "300px-Some_image.png" - if( UploadBase::isThumbName( $fileName ) ) { - //HINT: Thumbname-to-filename-conversion taken from includes/Upload/UploadBase.php - //Check for filenames like 50px- or 180px-, these are mostly thumbnails - $fileName = substr( $fileName , strpos( $fileName , '-' ) +1 ); - } - $title = Title::makeTitle( (int)$namespaceId, $fileName ); - if( $title instanceof Title && !$title->userCan( 'read' ) ) { - return false; - } - - //Maybe the single file is not protectey by Extension:Lockdown, but - //also by some other mechanism - $actualFileTitle = Title::makeTitle( NS_FILE, $title->getPrefixedText() ); - if( $actualFileTitle instanceof Title && !$actualFileTitle->userCan( 'read' ) ) { - return false; - } - - return parent::doGetFileStat( $params ); - } - - protected function resolveToFSPath( $storagePath ) { - global $wgUploadDirectory; - $unprefixedPath = $this->stripStorageBasePath( $storagePath ); - return "$wgUploadDirectory/$unprefixedPath"; - } - - /** + * Enables support for Non-ASCII filenames event on Windows. As we always deliver through a PHP script + * (e.g. img_auth.php) the encoding issue of PHP on the Windows FS is not a problem anymore. * @see FSFileBackend::getFeatures() * @return int */ public function getFeatures() { return FileBackend::ATTR_UNICODE_PATHS; - } - - protected function stripStorageBasePath( $storagePath ) { - global $wgImgAuthUrlPathMap; - /** - * TODO: Improve this! - * A proper FileBackendConfiguration should be used! - */ - foreach( $this->zoneSuffixes as $zone ) { - $parts = explode( "$zone/nsfilerepo/", $storagePath, 2 ); - if( count( $parts ) === 2 ) { - if( $zone === 'public' ) { - return $parts[1]; - } - return $zone.'/'.$parts[1]; - } - } - - return preg_replace( "#^{$wgImgAuthUrlPathMap['/nsfilerepo/']}#", '', $storagePath ); } } \ No newline at end of file diff --git a/includes/filerepo/file/NSLocalFile.php b/includes/filerepo/file/NSLocalFile.php index d39b69d..d2b1222 100644 --- a/includes/filerepo/file/NSLocalFile.php +++ b/includes/filerepo/file/NSLocalFile.php @@ -325,10 +325,6 @@ return false; } } - - public function getHashPath() { - return 'nsfilerepo/' . parent::getHashPath(); - } } /** diff --git a/includes/utility/NSFileRepoHelper.php b/includes/utility/NSFileRepoHelper.php new file mode 100644 index 0000000..f620ac5 --- /dev/null +++ b/includes/utility/NSFileRepoHelper.php @@ -0,0 +1,36 @@ +<?php + +class NSFileRepoHelper { + protected $pathRegEx = '#\\/(thumb\\/|archive\\/|deleted\\/)?(\d*?)?\\/[a-f0-9]{1}\\/[a-f0-9]{2}\\/(.*?)$#'; + + /** + * Returns a Title object that can be used to check permissions against. ATTENTION: It will _not_ return a + * Title from NS_FILE, but either from NS_MAIN, or from the specific namespace that was found in the path! + * Examples for $path: + * /thumb/1502/7/78/Some_File.png/300px-Some_File.png + * /1502/7/78/Some_File.png + * @param $path + * @return null|Title + */ + public function getTitleFromPath( $path ) { + error_log( $path ); + $filename = wfBaseName( $path ); + if( UploadBase::isThumbName( $filename ) ) { + //HINT: Thumbname-to-filename-conversion taken from includes/Upload/UploadBase.php + //Check for filenames like 50px- or 180px-, these are mostly thumbnails + $filename = substr( $filename , strpos( $filename , '-' ) +1 ); + } + + $title = Title::newFromText( $filename ); + + $matches = array(); + preg_match( $this->pathRegEx , $path, $matches ); + if( empty( $matches[2] ) ) { //Not a file from a namespace? + return $title; + } + + $title = Title::makeTitleSafe( (int)$matches[2], $filename ); + error_log( $title->getPrefixedDBkey() ); + return $title; + } +} \ No newline at end of file diff --git a/nsfr_img_auth.php b/nsfr_img_auth.php new file mode 100644 index 0000000..33e5423 --- /dev/null +++ b/nsfr_img_auth.php @@ -0,0 +1,224 @@ +<?php + +/** + * THIS SCRIPT IS A COPY OF MEDIAWIKI CORE img_auth.php ENTRY POINT + * It has been altered to enable NSFileRepo functionality in REL1_27 + */ + +/** + * Image authorisation script + * + * To use this, see https://www.mediawiki.org/wiki/Manual:Image_Authorization + * + * - Set $wgUploadDirectory to a non-public directory (not web accessible) + * - Set $wgUploadPath to point to this file + * + * Optional Parameters + * + * - Set $wgImgAuthDetails = true if you want the reason the access was denied messages to + * be displayed instead of just the 403 error (doesn't work on IE anyway), + * otherwise it will only appear in error logs + * + * For security reasons, you usually don't want your user to know *why* access was denied, + * just that it was. If you want to change this, you can set $wgImgAuthDetails to 'true' + * in localsettings.php and it will give the user the reason why access was denied. + * + * Your server needs to support PATH_INFO; CGI-based configurations usually don't. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); + +$baseDir = dirname( $_SERVER['SCRIPT_FILENAME'] ); +chdir( $baseDir ); +require ( $baseDir . '/includes/WebStart.php' ); +unset( $baseDir ); +//require ( __DIR__ . '/includes/WebStart.php' ); //This would be if we didn't work with a symlink + +# Set action base paths so that WebRequest::getPathInfo() +# recognizes the "X" as the 'title' in ../img_auth.php/X urls. +$wgArticlePath = false; # Don't let a "/*" article path clober our action path +$wgActionPaths = [ "$wgUploadPath/" ]; + +wfImageAuthMain(); + +$mediawiki = new MediaWiki(); +$mediawiki->doPostOutputShutdown( 'fast' ); + +function wfImageAuthMain() { + global $wgImgAuthUrlPathMap; + + $request = RequestContext::getMain()->getRequest(); + + // Get the requested file path (source file or thumbnail) + $matches = WebRequest::getPathInfo(); + if ( !isset( $matches['title'] ) ) { + wfForbidden( 'img-auth-accessdenied', 'img-auth-nopathinfo' ); + return; + } + $path = $matches['title']; + if ( $path && $path[0] !== '/' ) { + // Make sure $path has a leading / + $path = "/" . $path; + } + + // Check for bug 28235: QUERY_STRING overriding the correct extension + $whitelist = []; + $extension = FileBackend::extensionFromPath( $path, 'rawcase' ); + if ( $extension != '' ) { + $whitelist[] = $extension; + } + if ( !$request->checkUrlExtension( $whitelist ) ) { + return; + } + + // Various extensions may have their own backends that need access. + // Check if there is a special backend and storage base path for this file. + foreach ( $wgImgAuthUrlPathMap as $prefix => $storageDir ) { + $prefix = rtrim( $prefix, '/' ) . '/'; // implicit trailing slash + if ( strpos( $path, $prefix ) === 0 ) { + $be = FileBackendGroup::singleton()->backendFromPath( $storageDir ); + $filename = $storageDir . substr( $path, strlen( $prefix ) ); // strip prefix + // Check basic user authorization + if ( !RequestContext::getMain()->getUser()->isAllowed( 'read' ) ) { + wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $path ); + return; + } + if ( $be->fileExists( [ 'src' => $filename ] ) ) { + wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." ); + $be->streamFile( [ 'src' => $filename ], + [ 'Cache-Control: private', 'Vary: Cookie' ] ); + } else { + wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $path ); + } + return; + } + } + + // Get the local file repository + $repo = RepoGroup::singleton()->getRepo( 'local' ); + $zone = strstr( ltrim( $path, '/' ), '/', true ); + + // Get the full file storage path and extract the source file name. + // (e.g. 120px-Foo.png => Foo.png or page2-120px-Foo.png => Foo.png). + // This only applies to thumbnails/transcoded, and each of them should + // be under a folder that has the source file name. + if ( $zone === 'thumb' || $zone === 'transcoded' ) { + $name = wfBaseName( dirname( $path ) ); + $filename = $repo->getZonePath( $zone ) . substr( $path, strlen( "/" . $zone ) ); + Hooks::run( 'ImgAuthBeforeCheckFileExists', [ &$path, &$name, &$filename ] ); + // Check to see if the file exists + if ( !$repo->fileExists( $filename ) ) { + wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $filename ); + return; + } + } else { + $name = wfBaseName( $path ); // file is a source file + $filename = $repo->getZonePath( 'public' ) . $path; + Hooks::run( 'ImgAuthBeforeCheckFileExists', [ &$path, &$name, &$filename ] ); + // Check to see if the file exists and is not deleted + $bits = explode( '!', $name, 2 ); + if ( substr( $path, 0, 9 ) === '/archive/' && count( $bits ) == 2 ) { + $file = $repo->newFromArchiveName( $bits[1], $name ); + } else { + $file = $repo->newFile( $name ); + } + if ( !$file->exists() || $file->isDeleted( File::DELETED_FILE ) ) { + wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $filename ); + return; + } + } + + $headers = []; // extra HTTP headers to send + + // For private wikis, run extra auth checks and set cache control headers + $headers[] = 'Cache-Control: private'; + $headers[] = 'Vary: Cookie'; + + $title = Title::makeTitleSafe( NS_FILE, $name ); + if ( !$title instanceof Title ) { // files have valid titles + wfForbidden( 'img-auth-accessdenied', 'img-auth-badtitle', $name ); + return; + } + + // Run hook for extension authorization plugins + /** @var $result array */ + $result = null; + if ( !Hooks::run( 'ImgAuthBeforeStream', [ &$title, &$path, &$name, &$result ] ) ) { + wfForbidden( $result[0], $result[1], array_slice( $result, 2 ) ); + return; + } + + // Check user authorization for this title + // Checks Whitelist too + if ( !$title->userCan( 'read' ) ) { + wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $name ); + return; + } + + if ( $request->getCheck( 'download' ) ) { + $headers[] = 'Content-Disposition: attachment'; + } + + // Stream the requested file + wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." ); + $repo->streamFile( $filename, $headers ); +} + +/** + * Issue a standard HTTP 403 Forbidden header ($msg1-a message index, not a message) and an + * error message ($msg2, also a message index), (both required) then end the script + * subsequent arguments to $msg2 will be passed as parameters only for replacing in $msg2 + * @param string $msg1 + * @param string $msg2 + */ +function wfForbidden( $msg1, $msg2 ) { + global $wgImgAuthDetails; + + $args = func_get_args(); + array_shift( $args ); + array_shift( $args ); + $args = ( isset( $args[0] ) && is_array( $args[0] ) ) ? $args[0] : $args; + + $msgHdr = wfMessage( $msg1 )->escaped(); + $detailMsgKey = $wgImgAuthDetails ? $msg2 : 'badaccess-group0'; + $detailMsg = wfMessage( $detailMsgKey, $args )->escaped(); + + wfDebugLog( 'img_auth', + "wfForbidden Hdr: " . wfMessage( $msg1 )->inLanguage( 'en' )->text() . " Msg: " . + wfMessage( $msg2, $args )->inLanguage( 'en' )->text() + ); + + HttpStatus::header( 403 ); + header( 'Cache-Control: no-cache' ); + header( 'Content-Type: text/html; charset=utf-8' ); + echo <<<ENDS +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8" /> +<title>$msgHdr</title> +</head> +<body> +<h1>$msgHdr</h1> +<p>$detailMsg</p> +</body> +</html> +ENDS; +} -- To view, visit https://gerrit.wikimedia.org/r/326915 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I38843ba186bbb3e620c0b2e7e252dbdf9827f477 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/NSFileRepo Gerrit-Branch: master Gerrit-Owner: Robert Vogel <vo...@hallowelt.biz> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits