Index: trunk/extensions/DSMW/api/upload/UploadFromStash.php |
— | — | @@ -0,0 +1,84 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * @file |
| 5 | + * @ingroup upload |
| 6 | + * |
| 7 | + * Implements uploading from previously stored file. |
| 8 | + * |
| 9 | + * @author Bryan Tong Minh |
| 10 | + */ |
| 11 | + |
| 12 | +class UploadFromStash extends UploadBase { |
| 13 | + public static function isValidSessionKey( $key, $sessionData ) { |
| 14 | + return !empty( $key ) && |
| 15 | + is_array( $sessionData ) && |
| 16 | + isset( $sessionData[$key] ) && |
| 17 | + isset( $sessionData[$key]['version'] ) && |
| 18 | + $sessionData[$key]['version'] == self::SESSION_VERSION; |
| 19 | + } |
| 20 | + |
| 21 | + public static function isValidRequest( $request ) { |
| 22 | + $sessionData = $request->getSessionData( 'wsUploadData' ); |
| 23 | + return self::isValidSessionKey( |
| 24 | + $request->getInt( 'wpSessionKey' ), |
| 25 | + $sessionData |
| 26 | + ); |
| 27 | + } |
| 28 | + |
| 29 | + public function initialize( $name, $sessionKey, $sessionData ) { |
| 30 | + /** |
| 31 | + * Confirming a temporarily stashed upload. |
| 32 | + * We don't want path names to be forged, so we keep |
| 33 | + * them in the session on the server and just give |
| 34 | + * an opaque key to the user agent. |
| 35 | + */ |
| 36 | + |
| 37 | + $this->initializePathInfo( $name, |
| 38 | + $this->getRealPath ( $sessionData['mTempPath'] ), |
| 39 | + $sessionData['mFileSize'], |
| 40 | + false |
| 41 | + ); |
| 42 | + |
| 43 | + $this->mSessionKey = $sessionKey; |
| 44 | + $this->mVirtualTempPath = $sessionData['mTempPath']; |
| 45 | + $this->mFileProps = $sessionData['mFileProps']; |
| 46 | + } |
| 47 | + |
| 48 | + public function initializeFromRequest( &$request ) { |
| 49 | + $sessionKey = $request->getInt( 'wpSessionKey' ); |
| 50 | + $sessionData = $request->getSessionData('wsUploadData'); |
| 51 | + |
| 52 | + $desiredDestName = $request->getText( 'wpDestFile' ); |
| 53 | + if( !$desiredDestName ) |
| 54 | + $desiredDestName = $request->getText( 'wpUploadFile' ); |
| 55 | + return $this->initialize( $desiredDestName, $sessionKey, $sessionData[$sessionKey] ); |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * File has been previously verified so no need to do so again. |
| 60 | + */ |
| 61 | + protected function verifyFile() { |
| 62 | + return true; |
| 63 | + } |
| 64 | + |
| 65 | + |
| 66 | + /** |
| 67 | + * There is no need to stash the image twice |
| 68 | + */ |
| 69 | + public function stashSession() { |
| 70 | + if ( !empty( $this->mSessionKey ) ) |
| 71 | + return $this->mSessionKey; |
| 72 | + return parent::stashSession(); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Remove a temporarily kept file stashed by saveTempUploadedFile(). |
| 77 | + * @return success |
| 78 | + */ |
| 79 | + public function unsaveUploadedFile() { |
| 80 | + $repo = RepoGroup::singleton()->getLocalRepo(); |
| 81 | + $success = $repo->freeTemp( $this->mVirtualTempPath ); |
| 82 | + return $success; |
| 83 | + } |
| 84 | + |
| 85 | +} |
\ No newline at end of file |
Property changes on: trunk/extensions/DSMW/api/upload/UploadFromStash.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 86 | + native |
Index: trunk/extensions/DSMW/api/upload/UploadFromUrl.php |
— | — | @@ -0,0 +1,137 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * @file |
| 5 | + * @ingroup upload |
| 6 | + * |
| 7 | + * Implements uploading from a HTTP resource. |
| 8 | + * |
| 9 | + * @author Bryan Tong Minh |
| 10 | + * @author Michael Dale |
| 11 | + */ |
| 12 | +class UploadFromUrl extends UploadBase { |
| 13 | + protected $mTempDownloadPath; |
| 14 | + |
| 15 | + /** |
| 16 | + * Checks if the user is allowed to use the upload-by-URL feature. If the |
| 17 | + * user is allowed, pass on permissions checking to the parent. |
| 18 | + */ |
| 19 | + public static function isAllowed( $user ) { |
| 20 | + if( !$user->isAllowed( 'upload_by_url' ) ) |
| 21 | + return 'upload_by_url'; |
| 22 | + return parent::isAllowed( $user ); |
| 23 | + } |
| 24 | + |
| 25 | + /** |
| 26 | + * Checks if the upload from URL feature is enabled |
| 27 | + */ |
| 28 | + public static function isEnabled() { |
| 29 | + global $wgAllowCopyUploads; |
| 30 | + return $wgAllowCopyUploads && parent::isEnabled(); |
| 31 | + } |
| 32 | + |
| 33 | + /** |
| 34 | + * Entry point for API upload |
| 35 | + */ |
| 36 | + public function initialize( $name, $url, $na=false, $nb = false ) { |
| 37 | + global $wgTmpDirectory; |
| 38 | + |
| 39 | + $localFile = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); |
| 40 | + $this->initializePathInfo( $name, $localFile, 0, true ); |
| 41 | + |
| 42 | + $this->mUrl = trim( $url ); |
| 43 | + } |
| 44 | + |
| 45 | + /** |
| 46 | + * Entry point for SpecialUpload |
| 47 | + * @param $request Object: WebRequest object |
| 48 | + */ |
| 49 | + public function initializeFromRequest( &$request ) { |
| 50 | + $desiredDestName = $request->getText( 'wpDestFile' ); |
| 51 | + if( !$desiredDestName ) |
| 52 | + $desiredDestName = $request->getText( 'wpUploadFileURL' ); |
| 53 | + return $this->initialize( |
| 54 | + $desiredDestName, |
| 55 | + $request->getVal( 'wpUploadFileURL' ), |
| 56 | + false |
| 57 | + ); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * @param $request Object: WebRequest object |
| 62 | + */ |
| 63 | + public static function isValidRequest( $request ){ |
| 64 | + if( !$request->getVal( 'wpUploadFileURL' ) ) |
| 65 | + return false; |
| 66 | + // check that is a valid url: |
| 67 | + return self::isValidUrl( $request->getVal( 'wpUploadFileURL' ) ); |
| 68 | + } |
| 69 | + |
| 70 | + public static function isValidUrl( $url ) { |
| 71 | + // Only allow HTTP or FTP for now |
| 72 | + return (bool)preg_match( '!^(http://|ftp://)!', $url ); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Do the real fetching stuff |
| 77 | + */ |
| 78 | + function fetchFile() { |
| 79 | + if( !self::isValidUrl( $this->mUrl ) ) { |
| 80 | + return Status::newFatal( 'upload-proto-error' ); |
| 81 | + } |
| 82 | + $res = $this->curlCopy(); |
| 83 | + if( $res !== true ) { |
| 84 | + return Status::newFatal( $res ); |
| 85 | + } |
| 86 | + return Status::newGood(); |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Safe copy from URL |
| 91 | + * Returns true if there was an error, false otherwise |
| 92 | + */ |
| 93 | + private function curlCopy() { |
| 94 | + global $wgOut; |
| 95 | + |
| 96 | + # Open temporary file |
| 97 | + $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); |
| 98 | + if( $this->mCurlDestHandle === false ) { |
| 99 | + # Could not open temporary file to write in |
| 100 | + return 'upload-file-error'; |
| 101 | + } |
| 102 | + |
| 103 | + $ch = curl_init(); |
| 104 | + curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug |
| 105 | + curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout |
| 106 | + curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed |
| 107 | + curl_setopt( $ch, CURLOPT_URL, $this->mUrl); |
| 108 | + curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); |
| 109 | + curl_exec( $ch ); |
| 110 | + $error = curl_errno( $ch ); |
| 111 | + curl_close( $ch ); |
| 112 | + |
| 113 | + fclose( $this->mCurlDestHandle ); |
| 114 | + unset( $this->mCurlDestHandle ); |
| 115 | + |
| 116 | + if( $error ) |
| 117 | + return "upload-curl-error$errornum"; |
| 118 | + |
| 119 | + return true; |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * Callback function for CURL-based web transfer |
| 124 | + * Write data to file unless we've passed the length limit; |
| 125 | + * if so, abort immediately. |
| 126 | + * @access private |
| 127 | + */ |
| 128 | + function uploadCurlCallback( $ch, $data ) { |
| 129 | + global $wgMaxUploadSize; |
| 130 | + $length = strlen( $data ); |
| 131 | + $this->mFileSize += $length; |
| 132 | + if( $this->mFileSize > $wgMaxUploadSize ) { |
| 133 | + return 0; |
| 134 | + } |
| 135 | + fwrite( $this->mCurlDestHandle, $data ); |
| 136 | + return $length; |
| 137 | + } |
| 138 | +} |
Property changes on: trunk/extensions/DSMW/api/upload/UploadFromUrl.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 139 | + native |
Index: trunk/extensions/DSMW/api/upload/ApiQueryImageInfo.php |
— | — | @@ -0,0 +1,339 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/* |
| 5 | + * Created on July 6, 2007 |
| 6 | + * |
| 7 | + * API for MediaWiki 1.8+ |
| 8 | + * |
| 9 | + * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com |
| 10 | + * |
| 11 | + * This program is free software; you can redistribute it and/or modify |
| 12 | + * it under the terms of the GNU General Public License as published by |
| 13 | + * the Free Software Foundation; either version 2 of the License, or |
| 14 | + * (at your option) any later version. |
| 15 | + * |
| 16 | + * This program is distributed in the hope that it will be useful, |
| 17 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 18 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 19 | + * GNU General Public License for more details. |
| 20 | + * |
| 21 | + * You should have received a copy of the GNU General Public License along |
| 22 | + * with this program; if not, write to the Free Software Foundation, Inc., |
| 23 | + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 24 | + * http://www.gnu.org/copyleft/gpl.html |
| 25 | + */ |
| 26 | + |
| 27 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 28 | + // Eclipse helper - will be ignored in production |
| 29 | + require_once ( 'ApiQueryBase.php' ); |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * A query action to get image information and upload history. |
| 34 | + * |
| 35 | + * @ingroup API |
| 36 | + */ |
| 37 | +class ApiQueryImageInfo extends ApiQueryBase { |
| 38 | + |
| 39 | + public function __construct( $query, $moduleName ) { |
| 40 | + parent :: __construct( $query, $moduleName, 'ii' ); |
| 41 | + } |
| 42 | + |
| 43 | + public function execute() { |
| 44 | + $params = $this->extractRequestParams(); |
| 45 | + |
| 46 | + $prop = array_flip( $params['prop'] ); |
| 47 | + |
| 48 | + if ( $params['urlheight'] != - 1 && $params['urlwidth'] == - 1 ) |
| 49 | + $this->dieUsage( "iiurlheight cannot be used without iiurlwidth", 'iiurlwidth' ); |
| 50 | + |
| 51 | + if ( $params['urlwidth'] != - 1 ) { |
| 52 | + $scale = array(); |
| 53 | + $scale['width'] = $params['urlwidth']; |
| 54 | + $scale['height'] = $params['urlheight']; |
| 55 | + } else { |
| 56 | + $scale = null; |
| 57 | + } |
| 58 | + |
| 59 | + $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); |
| 60 | + if ( !empty( $pageIds[NS_FILE] ) ) { |
| 61 | + $titles = array_keys( $pageIds[NS_FILE] ); |
| 62 | + asort( $titles ); // Ensure the order is always the same |
| 63 | + |
| 64 | + $skip = false; |
| 65 | + if ( !is_null( $params['continue'] ) ) |
| 66 | + { |
| 67 | + $skip = true; |
| 68 | + $cont = explode( '|', $params['continue'] ); |
| 69 | + if ( count( $cont ) != 2 ) |
| 70 | + $this->dieUsage( "Invalid continue param. You should pass the original " . |
| 71 | + "value returned by the previous query", "_badcontinue" ); |
| 72 | + $fromTitle = strval( $cont[0] ); |
| 73 | + $fromTimestamp = $cont[1]; |
| 74 | + // Filter out any titles before $fromTitle |
| 75 | + foreach ( $titles as $key => $title ) |
| 76 | + if ( $title < $fromTitle ) |
| 77 | + unset( $titles[$key] ); |
| 78 | + else |
| 79 | + break; |
| 80 | + } |
| 81 | + |
| 82 | + $result = $this->getResult(); |
| 83 | + $images = RepoGroup::singleton()->findFiles( $titles ); |
| 84 | + foreach ( $images as $img ) { |
| 85 | + // Skip redirects |
| 86 | + if ( $img->getOriginalTitle()->isRedirect() ) |
| 87 | + continue; |
| 88 | + |
| 89 | + $start = $skip ? $fromTimestamp : $params['start']; |
| 90 | + $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; |
| 91 | + |
| 92 | + $fit = $result->addValue( |
| 93 | + array( 'query', 'pages', intval( $pageId ) ), |
| 94 | + 'imagerepository', $img->getRepoName() |
| 95 | + ); |
| 96 | + if ( !$fit ) |
| 97 | + { |
| 98 | + if ( count( $pageIds[NS_IMAGE] ) == 1 ) |
| 99 | + // The user is screwed. imageinfo can't be solely |
| 100 | + // responsible for exceeding the limit in this case, |
| 101 | + // so set a query-continue that just returns the same |
| 102 | + // thing again. When the violating queries have been |
| 103 | + // out-continued, the result will get through |
| 104 | + $this->setContinueEnumParameter( 'start', |
| 105 | + wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) ); |
| 106 | + else |
| 107 | + $this->setContinueEnumParameter( 'continue', |
| 108 | + $this->getContinueStr( $img ) ); |
| 109 | + break; |
| 110 | + } |
| 111 | + |
| 112 | + // Get information about the current version first |
| 113 | + // Check that the current version is within the start-end boundaries |
| 114 | + $gotOne = false; |
| 115 | + if ( ( is_null( $start ) || $img->getTimestamp() <= $start ) && |
| 116 | + ( is_null( $params['end'] ) || $img->getTimestamp() >= $params['end'] ) ) { |
| 117 | + $gotOne = true; |
| 118 | + $fit = $this->addPageSubItem( $pageId, |
| 119 | + self::getInfo( $img, $prop, $result, $scale ) ); |
| 120 | + if ( !$fit ) |
| 121 | + { |
| 122 | + if ( count( $pageIds[NS_IMAGE] ) == 1 ) |
| 123 | + // See the 'the user is screwed' comment above |
| 124 | + $this->setContinueEnumParameter( 'start', |
| 125 | + wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) ); |
| 126 | + else |
| 127 | + $this->setContinueEnumParameter( 'continue', |
| 128 | + $this->getContinueStr( $img ) ); |
| 129 | + break; |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + // Now get the old revisions |
| 134 | + // Get one more to facilitate query-continue functionality |
| 135 | + $count = ( $gotOne ? 1 : 0 ); |
| 136 | + $oldies = $img->getHistory( $params['limit'] - $count + 1, $start, $params['end'] ); |
| 137 | + foreach ( $oldies as $oldie ) { |
| 138 | + if ( ++$count > $params['limit'] ) { |
| 139 | + // We've reached the extra one which shows that there are additional pages to be had. Stop here... |
| 140 | + // Only set a query-continue if there was only one title |
| 141 | + if ( count( $pageIds[NS_FILE] ) == 1 ) |
| 142 | + { |
| 143 | + $this->setContinueEnumParameter( 'start', |
| 144 | + wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) ); |
| 145 | + } |
| 146 | + break; |
| 147 | + } |
| 148 | + $fit = $this->addPageSubItem( $pageId, |
| 149 | + self::getInfo( $oldie, $prop, $result ) ); |
| 150 | + if ( !$fit ) |
| 151 | + { |
| 152 | + if ( count( $pageIds[NS_IMAGE] ) == 1 ) |
| 153 | + $this->setContinueEnumParameter( 'start', |
| 154 | + wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) ); |
| 155 | + else |
| 156 | + $this->setContinueEnumParameter( 'continue', |
| 157 | + $this->getContinueStr( $oldie ) ); |
| 158 | + break; |
| 159 | + } |
| 160 | + } |
| 161 | + if ( !$fit ) |
| 162 | + break; |
| 163 | + $skip = false; |
| 164 | + } |
| 165 | + |
| 166 | + $data = $this->getResultData(); |
| 167 | + foreach ( $data['query']['pages'] as $pageid => $arr ) { |
| 168 | + if ( !isset( $arr['imagerepository'] ) ) |
| 169 | + $result->addValue( |
| 170 | + array( 'query', 'pages', $pageid ), |
| 171 | + 'imagerepository', '' |
| 172 | + ); |
| 173 | + // The above can't fail because it doesn't increase the result size |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + /** |
| 179 | + * Get result information for an image revision |
| 180 | + * @param File f The image |
| 181 | + * @return array Result array |
| 182 | + */ |
| 183 | + static function getInfo( $file, $prop, $result, $scale = null ) { |
| 184 | + $vals = array(); |
| 185 | + if ( isset( $prop['timestamp'] ) ) |
| 186 | + $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); |
| 187 | + if ( isset( $prop['user'] ) ) { |
| 188 | + $vals['user'] = $file->getUser(); |
| 189 | + if ( !$file->getUser( 'id' ) ) |
| 190 | + $vals['anon'] = ''; |
| 191 | + } |
| 192 | + if ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) { |
| 193 | + $vals['size'] = intval( $file->getSize() ); |
| 194 | + $vals['width'] = intval( $file->getWidth() ); |
| 195 | + $vals['height'] = intval( $file->getHeight() ); |
| 196 | + } |
| 197 | + if ( isset( $prop['url'] ) ) { |
| 198 | + if ( !is_null( $scale ) && !$file->isOld() ) { |
| 199 | + $mto = $file->transform( array( 'width' => $scale['width'], 'height' => $scale['height'] ) ); |
| 200 | + if ( $mto && !$mto->isError() ) |
| 201 | + { |
| 202 | + $vals['thumburl'] = wfExpandUrl( $mto->getUrl() ); |
| 203 | + $vals['thumbwidth'] = intval( $mto->getWidth() ); |
| 204 | + $vals['thumbheight'] = intval( $mto->getHeight() ); |
| 205 | + } |
| 206 | + } |
| 207 | + $vals['url'] = $file->getFullURL(); |
| 208 | + $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl() ); |
| 209 | + } |
| 210 | + if ( isset( $prop['comment'] ) ) |
| 211 | + $vals['comment'] = $file->getDescription(); |
| 212 | + if ( isset( $prop['sha1'] ) ) |
| 213 | + $vals['sha1'] = wfBaseConvert( $file->getSha1(), 36, 16, 40 ); |
| 214 | + if ( isset( $prop['metadata'] ) ) { |
| 215 | + $metadata = $file->getMetadata(); |
| 216 | + $vals['metadata'] = $metadata ? self::processMetaData( unserialize( $metadata ), $result ) : null; |
| 217 | + } |
| 218 | + if ( isset( $prop['mime'] ) ) |
| 219 | + $vals['mime'] = $file->getMimeType(); |
| 220 | + |
| 221 | + if ( isset( $prop['archivename'] ) && $file->isOld() ) |
| 222 | + $vals['archivename'] = $file->getArchiveName(); |
| 223 | + |
| 224 | + if ( isset( $prop['bitdepth'] ) ) |
| 225 | + $vals['bitdepth'] = $file->getBitDepth(); |
| 226 | + |
| 227 | + return $vals; |
| 228 | + } |
| 229 | + |
| 230 | + public static function processMetaData( $metadata, $result ) |
| 231 | + { |
| 232 | + $retval = array(); |
| 233 | + if ( is_array( $metadata ) ) { |
| 234 | + foreach ( $metadata as $key => $value ) |
| 235 | + { |
| 236 | + $r = array( 'name' => $key ); |
| 237 | + if ( is_array( $value ) ) |
| 238 | + $r['value'] = self::processMetaData( $value, $result ); |
| 239 | + else |
| 240 | + $r['value'] = $value; |
| 241 | + $retval[] = $r; |
| 242 | + } |
| 243 | + } |
| 244 | + $result->setIndexedTagName( $retval, 'metadata' ); |
| 245 | + return $retval; |
| 246 | + } |
| 247 | + |
| 248 | + private function getContinueStr( $img ) |
| 249 | + { |
| 250 | + return $img->getOriginalTitle()->getText() . |
| 251 | + '|' . $img->getTimestamp(); |
| 252 | + } |
| 253 | + |
| 254 | + public function getAllowedParams() { |
| 255 | + return array ( |
| 256 | + 'prop' => array ( |
| 257 | + ApiBase :: PARAM_ISMULTI => true, |
| 258 | + ApiBase :: PARAM_DFLT => 'timestamp|user', |
| 259 | + ApiBase :: PARAM_TYPE => self::getPropertyNames() |
| 260 | + ), |
| 261 | + 'limit' => array( |
| 262 | + ApiBase :: PARAM_TYPE => 'limit', |
| 263 | + ApiBase :: PARAM_DFLT => 1, |
| 264 | + ApiBase :: PARAM_MIN => 1, |
| 265 | + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, |
| 266 | + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 |
| 267 | + ), |
| 268 | + 'start' => array( |
| 269 | + ApiBase :: PARAM_TYPE => 'timestamp' |
| 270 | + ), |
| 271 | + 'end' => array( |
| 272 | + ApiBase :: PARAM_TYPE => 'timestamp' |
| 273 | + ), |
| 274 | + 'urlwidth' => array( |
| 275 | + ApiBase :: PARAM_TYPE => 'integer', |
| 276 | + ApiBase :: PARAM_DFLT => - 1 |
| 277 | + ), |
| 278 | + 'urlheight' => array( |
| 279 | + ApiBase :: PARAM_TYPE => 'integer', |
| 280 | + ApiBase :: PARAM_DFLT => - 1 |
| 281 | + ), |
| 282 | + 'continue' => null, |
| 283 | + ); |
| 284 | + } |
| 285 | + |
| 286 | + /** |
| 287 | + * Returns all possible parameters to iiprop |
| 288 | + */ |
| 289 | + public static function getPropertyNames() { |
| 290 | + return array ( |
| 291 | + 'timestamp', |
| 292 | + 'user', |
| 293 | + 'comment', |
| 294 | + 'url', |
| 295 | + 'size', |
| 296 | + 'dimensions', // For backwards compatibility with Allimages |
| 297 | + 'sha1', |
| 298 | + 'mime', |
| 299 | + 'metadata', |
| 300 | + 'archivename', |
| 301 | + 'bitdepth', |
| 302 | + ); |
| 303 | + } |
| 304 | + |
| 305 | + public function getParamDescription() { |
| 306 | + return array ( |
| 307 | + 'prop' => 'What image information to get.', |
| 308 | + 'limit' => 'How many image revisions to return', |
| 309 | + 'start' => 'Timestamp to start listing from', |
| 310 | + 'end' => 'Timestamp to stop listing at', |
| 311 | + 'urlwidth' => array( 'If iiprop=url is set, a URL to an image scaled to this width will be returned.', |
| 312 | + 'Only the current version of the image can be scaled.' ), |
| 313 | + 'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth', |
| 314 | + 'continue' => 'When more results are available, use this to continue', |
| 315 | + ); |
| 316 | + } |
| 317 | + |
| 318 | + public function getDescription() { |
| 319 | + return array ( |
| 320 | + 'Returns image information and upload history' |
| 321 | + ); |
| 322 | + } |
| 323 | + |
| 324 | + public function getPossibleErrors() { |
| 325 | + return array_merge( parent::getPossibleErrors(), array( |
| 326 | + array( 'code' => 'iiurlwidth', 'info' => 'iiurlheight cannot be used without iiurlwidth' ), |
| 327 | + ) ); |
| 328 | + } |
| 329 | + |
| 330 | + protected function getExamples() { |
| 331 | + return array ( |
| 332 | + 'api.php?action=query&titles=File:Albert%20Einstein%20Head.jpg&prop=imageinfo', |
| 333 | + 'api.php?action=query&titles=File:Test.jpg&prop=imageinfo&iilimit=50&iiend=20071231235959&iiprop=timestamp|user|url', |
| 334 | + ); |
| 335 | + } |
| 336 | + |
| 337 | + public function getVersion() { |
| 338 | + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 62415 2010-02-13 01:41:37Z reedy $'; |
| 339 | + } |
| 340 | +} |
Property changes on: trunk/extensions/DSMW/api/upload/ApiQueryImageInfo.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 341 | + native |
Index: trunk/extensions/DSMW/api/upload/ApiUpload.php |
— | — | @@ -0,0 +1,325 @@ |
| 2 | +<?php |
| 3 | +/* |
| 4 | + * Created on Aug 21, 2008 |
| 5 | + * API for MediaWiki 1.8+ |
| 6 | + * |
| 7 | + * Copyright (C) 2008 - 2010 Bryan Tong Minh <Bryan.TongMinh@Gmail.com> |
| 8 | + * |
| 9 | + * This program is free software; you can redistribute it and/or modify |
| 10 | + * it under the terms of the GNU General Public License as published by |
| 11 | + * the Free Software Foundation; either version 2 of the License, or |
| 12 | + * (at your option) any later version. |
| 13 | + * |
| 14 | + * This program is distributed in the hope that it will be useful, |
| 15 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 16 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 17 | + * GNU General Public License for more details. |
| 18 | + * |
| 19 | + * You should have received a copy of the GNU General Public License along |
| 20 | + * with this program; if not, write to the Free Software Foundation, Inc., |
| 21 | + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 22 | + * http://www.gnu.org/copyleft/gpl.html |
| 23 | + */ |
| 24 | + |
| 25 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 26 | + // Eclipse helper - will be ignored in production |
| 27 | + require_once( "ApiBase.php" ); |
| 28 | +} |
| 29 | + |
| 30 | +/** |
| 31 | + * @ingroup API |
| 32 | + */ |
| 33 | +class ApiUpload extends ApiBase { |
| 34 | + protected $mUpload = null; |
| 35 | + protected $mParams; |
| 36 | + |
| 37 | + public function __construct( $main, $action ) { |
| 38 | + parent::__construct( $main, $action ); |
| 39 | + } |
| 40 | + |
| 41 | + public function execute() { |
| 42 | + global $wgUser, $wgAllowCopyUploads; |
| 43 | + |
| 44 | + // Check whether upload is enabled |
| 45 | + if ( !UploadBase::isEnabled() ) |
| 46 | + $this->dieUsageMsg( array( 'uploaddisabled' ) ); |
| 47 | + |
| 48 | + $this->mParams = $this->extractRequestParams(); |
| 49 | + $request = $this->getMain()->getRequest(); |
| 50 | + |
| 51 | + // Add the uploaded file to the params array |
| 52 | + $this->mParams['file'] = $request->getFileName( 'file' ); |
| 53 | + |
| 54 | + // One and only one of the following parameters is needed |
| 55 | + $this->requireOnlyOneParameter( $this->mParams, |
| 56 | + 'sessionkey', 'file', 'url' ); |
| 57 | + |
| 58 | + if ( $this->mParams['sessionkey'] ) { |
| 59 | + /** |
| 60 | + * Upload stashed in a previous request |
| 61 | + */ |
| 62 | + // Check the session key |
| 63 | + if ( !isset( $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ) ) |
| 64 | + $this->dieUsageMsg( array( 'invalid-session-key' ) ); |
| 65 | + |
| 66 | + $this->mUpload = new UploadFromStash(); |
| 67 | + $this->mUpload->initialize( $this->mParams['filename'], |
| 68 | + $this->mParams['sessionkey'], |
| 69 | + $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ); |
| 70 | + } elseif ( isset( $this->mParams['filename'] ) ) { |
| 71 | + /** |
| 72 | + * Upload from url, etc |
| 73 | + * Parameter filename is required |
| 74 | + */ |
| 75 | + |
| 76 | + if ( isset( $this->mParams['file'] ) ) { |
| 77 | + $this->mUpload = new UploadFromFile(); |
| 78 | + $this->mUpload->initialize( |
| 79 | + $this->mParams['filename'], |
| 80 | + $request->getFileTempName( 'file' ), |
| 81 | + $request->getFileSize( 'file' ) |
| 82 | + ); |
| 83 | + } elseif ( isset( $this->mParams['url'] ) ) { |
| 84 | + // make sure upload by url is enabled: |
| 85 | + if ( !$wgAllowCopyUploads ) |
| 86 | + $this->dieUsageMsg( array( 'uploaddisabled' ) ); |
| 87 | + |
| 88 | + // make sure the current user can upload |
| 89 | + if ( ! $wgUser->isAllowed( 'upload_by_url' ) ) |
| 90 | + $this->dieUsageMsg( array( 'badaccess-groups' ) ); |
| 91 | + |
| 92 | + $this->mUpload = new UploadFromUrl(); |
| 93 | + $this->mUpload->initialize( $this->mParams['filename'], |
| 94 | + $this->mParams['url'] ); |
| 95 | + |
| 96 | + $status = $this->mUpload->fetchFile(); |
| 97 | + if ( !$status->isOK() ) { |
| 98 | + $this->dieUsage( $status->getWikiText(), 'fetchfileerror' ); |
| 99 | + } |
| 100 | + } |
| 101 | + } else $this->dieUsageMsg( array( 'missingparam', 'filename' ) ); |
| 102 | + |
| 103 | + if ( !isset( $this->mUpload ) ) |
| 104 | + $this->dieUsage( 'No upload module set', 'nomodule' ); |
| 105 | + |
| 106 | + // Check whether the user has the appropriate permissions to upload anyway |
| 107 | + $permission = $this->mUpload->isAllowed( $wgUser ); |
| 108 | + |
| 109 | + if ( $permission !== true ) { |
| 110 | + if ( !$wgUser->isLoggedIn() ) |
| 111 | + $this->dieUsageMsg( array( 'mustbeloggedin', 'upload' ) ); |
| 112 | + else |
| 113 | + $this->dieUsageMsg( array( 'badaccess-groups' ) ); |
| 114 | + } |
| 115 | + // Perform the upload |
| 116 | + $result = $this->performUpload(); |
| 117 | + |
| 118 | + // Cleanup any temporary mess |
| 119 | + $this->mUpload->cleanupTempFile(); |
| 120 | + |
| 121 | + $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
| 122 | + } |
| 123 | + |
| 124 | + protected function performUpload() { |
| 125 | + global $wgUser; |
| 126 | + $result = array(); |
| 127 | + $permErrors = $this->mUpload->verifyPermissions( $wgUser ); |
| 128 | + if ( $permErrors !== true ) { |
| 129 | + $this->dieUsageMsg( array( 'badaccess-groups' ) ); |
| 130 | + } |
| 131 | + |
| 132 | + // TODO: Move them to ApiBase's message map |
| 133 | + $verification = $this->mUpload->verifyUpload(); |
| 134 | + if ( $verification['status'] !== UploadBase::OK ) { |
| 135 | + $result['result'] = 'Failure'; |
| 136 | + switch( $verification['status'] ) { |
| 137 | + case UploadBase::EMPTY_FILE: |
| 138 | + $this->dieUsage( 'The file you submitted was empty', 'empty-file' ); |
| 139 | + break; |
| 140 | + case UploadBase::FILETYPE_MISSING: |
| 141 | + $this->dieUsage( 'The file is missing an extension', 'filetype-missing' ); |
| 142 | + break; |
| 143 | + case UploadBase::FILETYPE_BADTYPE: |
| 144 | + global $wgFileExtensions; |
| 145 | + $this->dieUsage( 'This type of file is banned', 'filetype-banned', |
| 146 | + 0, array( |
| 147 | + 'filetype' => $verification['finalExt'], |
| 148 | + 'allowed' => $wgFileExtensions |
| 149 | + ) ); |
| 150 | + break; |
| 151 | + case UploadBase::MIN_LENGTH_PARTNAME: |
| 152 | + $this->dieUsage( 'The filename is too short', 'filename-tooshort' ); |
| 153 | + break; |
| 154 | + case UploadBase::ILLEGAL_FILENAME: |
| 155 | + $this->dieUsage( 'The filename is not allowed', 'illegal-filename', |
| 156 | + 0, array( 'filename' => $verification['filtered'] ) ); |
| 157 | + break; |
| 158 | + case UploadBase::OVERWRITE_EXISTING_FILE: |
| 159 | + $this->dieUsage( 'Overwriting an existing file is not allowed', 'overwrite' ); |
| 160 | + break; |
| 161 | + case UploadBase::VERIFICATION_ERROR: |
| 162 | + $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); |
| 163 | + $this->dieUsage( 'This file did not pass file verification', 'verification-error', |
| 164 | + 0, array( 'details' => $verification['details'] ) ); |
| 165 | + break; |
| 166 | + case UploadBase::HOOK_ABORTED: |
| 167 | + $this->dieUsage( "The modification you tried to make was aborted by an extension hook", |
| 168 | + 'hookaborted', 0, array( 'error' => $verification['error'] ) ); |
| 169 | + break; |
| 170 | + default: |
| 171 | + $this->dieUsage( 'An unknown error occurred', 'unknown-error', |
| 172 | + 0, array( 'code' => $verification['status'] ) ); |
| 173 | + break; |
| 174 | + } |
| 175 | + return $result; |
| 176 | + } |
| 177 | + if ( !$this->mParams['ignorewarnings'] ) { |
| 178 | + $warnings = $this->mUpload->checkWarnings(); |
| 179 | + if ( $warnings ) { |
| 180 | + // Add indices |
| 181 | + $this->getResult()->setIndexedTagName( $warnings, 'warning' ); |
| 182 | + |
| 183 | + if ( isset( $warnings['duplicate'] ) ) { |
| 184 | + $dupes = array(); |
| 185 | + foreach ( $warnings['duplicate'] as $key => $dupe ) |
| 186 | + $dupes[] = $dupe->getName(); |
| 187 | + $this->getResult()->setIndexedTagName( $dupes, 'duplicate' ); |
| 188 | + $warnings['duplicate'] = $dupes; |
| 189 | + } |
| 190 | + |
| 191 | + |
| 192 | + if ( isset( $warnings['exists'] ) ) { |
| 193 | + $warning = $warnings['exists']; |
| 194 | + unset( $warnings['exists'] ); |
| 195 | + $warnings[$warning['warning']] = $warning['file']->getName(); |
| 196 | + } |
| 197 | + |
| 198 | + $result['result'] = 'Warning'; |
| 199 | + $result['warnings'] = $warnings; |
| 200 | + |
| 201 | + $sessionKey = $this->mUpload->stashSession(); |
| 202 | + if ( !$sessionKey ) |
| 203 | + $this->dieUsage( 'Stashing temporary file failed', 'stashfailed' ); |
| 204 | + |
| 205 | + $result['sessionkey'] = $sessionKey; |
| 206 | + |
| 207 | + return $result; |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + // Use comment as initial page text by default |
| 212 | + if ( is_null( $this->mParams['text'] ) ) |
| 213 | + $this->mParams['text'] = $this->mParams['comment']; |
| 214 | + |
| 215 | + // No errors, no warnings: do the upload |
| 216 | + $status = $this->mUpload->performUpload( $this->mParams['comment'], |
| 217 | + $this->mParams['text'], $this->mParams['watch'], $wgUser ); |
| 218 | + |
| 219 | + if ( !$status->isGood() ) { |
| 220 | + $error = $status->getErrorsArray(); |
| 221 | + $this->getResult()->setIndexedTagName( $result['details'], 'error' ); |
| 222 | + |
| 223 | + $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); |
| 224 | + } |
| 225 | + |
| 226 | + $file = $this->mUpload->getLocalFile(); |
| 227 | + $result['result'] = 'Success'; |
| 228 | + $result['filename'] = $file->getName(); |
| 229 | + $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); |
| 230 | + |
| 231 | + return $result; |
| 232 | + } |
| 233 | + |
| 234 | + public function mustBePosted() { |
| 235 | + return true; |
| 236 | + } |
| 237 | + |
| 238 | + public function isWriteMode() { |
| 239 | + return true; |
| 240 | + } |
| 241 | + |
| 242 | + public function getAllowedParams() { |
| 243 | + $params = array( |
| 244 | + 'filename' => null, |
| 245 | + 'comment' => array( |
| 246 | + ApiBase::PARAM_DFLT => '' |
| 247 | + ), |
| 248 | + 'text' => null, |
| 249 | + 'token' => null, |
| 250 | + 'watch' => false, |
| 251 | + 'ignorewarnings' => false, |
| 252 | + 'file' => null, |
| 253 | + 'url' => null, |
| 254 | + 'sessionkey' => null, |
| 255 | + ); |
| 256 | + return $params; |
| 257 | + |
| 258 | + } |
| 259 | + |
| 260 | + public function getParamDescription() { |
| 261 | + return array( |
| 262 | + 'filename' => 'Target filename', |
| 263 | + 'token' => 'Edit token. You can get one of these through prop=info', |
| 264 | + 'comment' => 'Upload comment. Also used as the initial page text for new files if "text" is not specified', |
| 265 | + 'text' => 'Initial page text for new files', |
| 266 | + 'watch' => 'Watch the page', |
| 267 | + 'ignorewarnings' => 'Ignore any warnings', |
| 268 | + 'file' => 'File contents', |
| 269 | + 'url' => 'Url to fetch the file from', |
| 270 | + 'sessionkey' => array( |
| 271 | + 'Session key returned by a previous upload that failed due to warnings', |
| 272 | + ), |
| 273 | + ); |
| 274 | + } |
| 275 | + |
| 276 | + public function getDescription() { |
| 277 | + return array( |
| 278 | + 'Upload a file, or get the status of pending uploads. Several methods are available:', |
| 279 | + ' * Upload file contents directly, using the "file" parameter', |
| 280 | + ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', |
| 281 | + ' * Complete an earlier upload that failed due to warnings, using the "sessionkey" parameter', |
| 282 | + 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', |
| 283 | + 'sending the "file". Note also that queries using session keys must be', |
| 284 | + 'done in the same login session as the query that originally returned the key (i.e. do not', |
| 285 | + 'log out and then log back in). Also you must get and send an edit token before doing any upload stuff.' |
| 286 | + ); |
| 287 | + } |
| 288 | + |
| 289 | + public function getPossibleErrors() { |
| 290 | + return array_merge( parent::getPossibleErrors(), array( |
| 291 | + array( 'uploaddisabled' ), |
| 292 | + array( 'invalid-session-key' ), |
| 293 | + array( 'uploaddisabled' ), |
| 294 | + array( 'badaccess-groups' ), |
| 295 | + array( 'missingparam', 'filename' ), |
| 296 | + array( 'mustbeloggedin', 'upload' ), |
| 297 | + array( 'badaccess-groups' ), |
| 298 | + array( 'badaccess-groups' ), |
| 299 | + array( 'code' => 'fetchfileerror', 'info' => '' ), |
| 300 | + array( 'code' => 'nomodule', 'info' => 'No upload module set' ), |
| 301 | + array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ), |
| 302 | + array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), |
| 303 | + array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), |
| 304 | + array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), |
| 305 | + array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), |
| 306 | + array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), |
| 307 | + ) ); |
| 308 | + } |
| 309 | + |
| 310 | + public function getTokenSalt() { |
| 311 | + return ''; |
| 312 | + } |
| 313 | + |
| 314 | + protected function getExamples() { |
| 315 | + return array( |
| 316 | + 'Upload from a URL:', |
| 317 | + ' api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png', |
| 318 | + 'Complete an upload that failed due to warnings:', |
| 319 | + ' api.php?action=upload&filename=Wiki.png&sessionkey=sessionkey&ignorewarnings=1', |
| 320 | + ); |
| 321 | + } |
| 322 | + |
| 323 | + public function getVersion() { |
| 324 | + return __CLASS__ . ': $Id: ApiUpload.php 51812 2009-06-12 23:45:20Z dale $'; |
| 325 | + } |
| 326 | +} |
Property changes on: trunk/extensions/DSMW/api/upload/ApiUpload.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 327 | + native |
Index: trunk/extensions/DSMW/api/upload/UploadBase.php |
— | — | @@ -0,0 +1,1091 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * @file |
| 5 | + * @ingroup upload |
| 6 | + * |
| 7 | + * UploadBase and subclasses are the backend of MediaWiki's file uploads. |
| 8 | + * The frontends are formed by ApiUpload and SpecialUpload. |
| 9 | + * |
| 10 | + * See also includes/docs/upload.txt |
| 11 | + * |
| 12 | + * @author Brion Vibber |
| 13 | + * @author Bryan Tong Minh |
| 14 | + * @author Michael Dale |
| 15 | + */ |
| 16 | + |
| 17 | +abstract class UploadBase { |
| 18 | + protected $mTempPath; |
| 19 | + protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; |
| 20 | + protected $mTitle = false, $mTitleError = 0; |
| 21 | + protected $mFilteredName, $mFinalExtension; |
| 22 | + protected $mLocalFile; |
| 23 | + |
| 24 | + const SUCCESS = 0; |
| 25 | + const OK = 0; |
| 26 | + const EMPTY_FILE = 3; |
| 27 | + const MIN_LENGTH_PARTNAME = 4; |
| 28 | + const ILLEGAL_FILENAME = 5; |
| 29 | + const OVERWRITE_EXISTING_FILE = 7; |
| 30 | + const FILETYPE_MISSING = 8; |
| 31 | + const FILETYPE_BADTYPE = 9; |
| 32 | + const VERIFICATION_ERROR = 10; |
| 33 | + const UPLOAD_VERIFICATION_ERROR = 11; |
| 34 | + const HOOK_ABORTED = 11; |
| 35 | + |
| 36 | + const SESSION_VERSION = 2; |
| 37 | + |
| 38 | + /** |
| 39 | + * Returns true if uploads are enabled. |
| 40 | + * Can be override by subclasses. |
| 41 | + */ |
| 42 | + public static function isEnabled() { |
| 43 | + global $wgEnableUploads; |
| 44 | + if ( !$wgEnableUploads ) { |
| 45 | + return false; |
| 46 | + } |
| 47 | + |
| 48 | + # Check php's file_uploads setting |
| 49 | + if( !wfIniGetBool( 'file_uploads' ) ) { |
| 50 | + return false; |
| 51 | + } |
| 52 | + return true; |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Returns true if the user can use this upload module or else a string |
| 57 | + * identifying the missing permission. |
| 58 | + * Can be overriden by subclasses. |
| 59 | + */ |
| 60 | + public static function isAllowed( $user ) { |
| 61 | + if( !$user->isAllowed( 'upload' ) ) { |
| 62 | + return 'upload'; |
| 63 | + } |
| 64 | + return true; |
| 65 | + } |
| 66 | + |
| 67 | + // Upload handlers. Should probably just be a global. |
| 68 | + static $uploadHandlers = array( 'Stash', 'File', 'Url' ); |
| 69 | + |
| 70 | + /** |
| 71 | + * Create a form of UploadBase depending on wpSourceType and initializes it |
| 72 | + */ |
| 73 | + public static function createFromRequest( &$request, $type = null ) { |
| 74 | + $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' ); |
| 75 | + |
| 76 | + if( !$type ) { |
| 77 | + return null; |
| 78 | + } |
| 79 | + |
| 80 | + // Get the upload class |
| 81 | + $type = ucfirst( $type ); |
| 82 | + |
| 83 | + // Give hooks the chance to handle this request |
| 84 | + $className = null; |
| 85 | + wfRunHooks( 'UploadCreateFromRequest', array( $type, &$className ) ); |
| 86 | + if ( is_null( $className ) ) { |
| 87 | + $className = 'UploadFrom' . $type; |
| 88 | + wfDebug( __METHOD__ . ": class name: $className\n" ); |
| 89 | + if( !in_array( $type, self::$uploadHandlers ) ) { |
| 90 | + return null; |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + // Check whether this upload class is enabled |
| 95 | + if( !call_user_func( array( $className, 'isEnabled' ) ) ) { |
| 96 | + return null; |
| 97 | + } |
| 98 | + |
| 99 | + // Check whether the request is valid |
| 100 | + if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) { |
| 101 | + return null; |
| 102 | + } |
| 103 | + |
| 104 | + $handler = new $className; |
| 105 | + |
| 106 | + $handler->initializeFromRequest( $request ); |
| 107 | + return $handler; |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Check whether a request if valid for this handler |
| 112 | + */ |
| 113 | + public static function isValidRequest( $request ) { |
| 114 | + return false; |
| 115 | + } |
| 116 | + |
| 117 | + public function __construct() {} |
| 118 | + |
| 119 | + /** |
| 120 | + * Initialize the path information |
| 121 | + * @param $name string the desired destination name |
| 122 | + * @param $tempPath string the temporary path |
| 123 | + * @param $fileSize int the file size |
| 124 | + * @param $removeTempFile bool (false) remove the temporary file? |
| 125 | + * @return null |
| 126 | + */ |
| 127 | + public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) { |
| 128 | + $this->mDesiredDestName = $name; |
| 129 | + $this->mTempPath = $tempPath; |
| 130 | + $this->mFileSize = $fileSize; |
| 131 | + $this->mRemoveTempFile = $removeTempFile; |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Initialize from a WebRequest. Override this in a subclass. |
| 136 | + */ |
| 137 | + public abstract function initializeFromRequest( &$request ); |
| 138 | + |
| 139 | + /** |
| 140 | + * Fetch the file. Usually a no-op |
| 141 | + */ |
| 142 | + public function fetchFile() { |
| 143 | + return Status::newGood(); |
| 144 | + } |
| 145 | + |
| 146 | + /** |
| 147 | + * Return the file size |
| 148 | + */ |
| 149 | + public function isEmptyFile() { |
| 150 | + return empty( $this->mFileSize ); |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * @param string $srcPath the source path |
| 155 | + * @returns the real path if it was a virtual URL |
| 156 | + */ |
| 157 | + function getRealPath( $srcPath ) { |
| 158 | + $repo = RepoGroup::singleton()->getLocalRepo(); |
| 159 | + if ( $repo->isVirtualUrl( $srcPath ) ) { |
| 160 | + return $repo->resolveVirtualUrl( $srcPath ); |
| 161 | + } |
| 162 | + return $srcPath; |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * Verify whether the upload is sane. |
| 167 | + * Returns self::OK or else an array with error information |
| 168 | + */ |
| 169 | + public function verifyUpload() { |
| 170 | + /** |
| 171 | + * If there was no filename or a zero size given, give up quick. |
| 172 | + */ |
| 173 | + if( $this->isEmptyFile() ) { |
| 174 | + return array( 'status' => self::EMPTY_FILE ); |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * Look at the contents of the file; if we can recognize the |
| 179 | + * type but it's corrupt or data of the wrong type, we should |
| 180 | + * probably not accept it. |
| 181 | + */ |
| 182 | + $verification = $this->verifyFile(); |
| 183 | + if( $verification !== true ) { |
| 184 | + if( !is_array( $verification ) ) { |
| 185 | + $verification = array( $verification ); |
| 186 | + } |
| 187 | + return array( |
| 188 | + 'status' => self::VERIFICATION_ERROR, |
| 189 | + 'details' => $verification |
| 190 | + ); |
| 191 | + } |
| 192 | + |
| 193 | + $nt = $this->getTitle(); |
| 194 | + if( is_null( $nt ) ) { |
| 195 | + $result = array( 'status' => $this->mTitleError ); |
| 196 | + if( $this->mTitleError == self::ILLEGAL_FILENAME ) { |
| 197 | + $result['filtered'] = $this->mFilteredName; |
| 198 | + } |
| 199 | + if ( $this->mTitleError == self::FILETYPE_BADTYPE ) { |
| 200 | + $result['finalExt'] = $this->mFinalExtension; |
| 201 | + } |
| 202 | + return $result; |
| 203 | + } |
| 204 | + $this->mDestName = $this->getLocalFile()->getName(); |
| 205 | + |
| 206 | + /** |
| 207 | + * In some cases we may forbid overwriting of existing files. |
| 208 | + */ |
| 209 | + $overwrite = $this->checkOverwrite(); |
| 210 | + if( $overwrite !== true ) { |
| 211 | + return array( |
| 212 | + 'status' => self::OVERWRITE_EXISTING_FILE, |
| 213 | + 'overwrite' => $overwrite |
| 214 | + ); |
| 215 | + } |
| 216 | + |
| 217 | + $error = ''; |
| 218 | + if( !wfRunHooks( 'UploadVerification', |
| 219 | + array( $this->mDestName, $this->mTempPath, &$error ) ) ) { |
| 220 | + // This status needs another name... |
| 221 | + return array( 'status' => self::HOOK_ABORTED, 'error' => $error ); |
| 222 | + } |
| 223 | + |
| 224 | + return array( 'status' => self::OK ); |
| 225 | + } |
| 226 | + |
| 227 | + /** |
| 228 | + * Verifies that it's ok to include the uploaded file |
| 229 | + * |
| 230 | + * @return mixed true of the file is verified, a string or array otherwise. |
| 231 | + */ |
| 232 | + protected function verifyFile() { |
| 233 | + $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); |
| 234 | + $this->checkMacBinary(); |
| 235 | + |
| 236 | + # magically determine mime type |
| 237 | + $magic = MimeMagic::singleton(); |
| 238 | + $mime = $magic->guessMimeType( $this->mTempPath, false ); |
| 239 | + |
| 240 | + # check mime type, if desired |
| 241 | + global $wgVerifyMimeType; |
| 242 | + if ( $wgVerifyMimeType ) { |
| 243 | + wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); |
| 244 | + if ( !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { |
| 245 | + return array( 'filetype-mime-mismatch' ); |
| 246 | + } |
| 247 | + |
| 248 | + global $wgMimeTypeBlacklist; |
| 249 | + if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { |
| 250 | + return array( 'filetype-badmime', $mime ); |
| 251 | + } |
| 252 | + |
| 253 | + # Check IE type |
| 254 | + $fp = fopen( $this->mTempPath, 'rb' ); |
| 255 | + $chunk = fread( $fp, 256 ); |
| 256 | + fclose( $fp ); |
| 257 | + $extMime = $magic->guessTypesForExtension( $this->mFinalExtension ); |
| 258 | + $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime ); |
| 259 | + foreach ( $ieTypes as $ieType ) { |
| 260 | + if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) { |
| 261 | + return array( 'filetype-bad-ie-mime', $ieType ); |
| 262 | + } |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + # check for htmlish code and javascript |
| 267 | + if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) { |
| 268 | + return 'uploadscripted'; |
| 269 | + } |
| 270 | + if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) { |
| 271 | + if( self::detectScriptInSvg( $this->mTempPath ) ) { |
| 272 | + return 'uploadscripted'; |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + /** |
| 277 | + * Scan the uploaded file for viruses |
| 278 | + */ |
| 279 | + $virus = $this->detectVirus( $this->mTempPath ); |
| 280 | + if ( $virus ) { |
| 281 | + return array( 'uploadvirus', $virus ); |
| 282 | + } |
| 283 | + wfDebug( __METHOD__ . ": all clear; passing.\n" ); |
| 284 | + return true; |
| 285 | + } |
| 286 | + |
| 287 | + /** |
| 288 | + * Check whether the user can edit, upload and create the image. |
| 289 | + * |
| 290 | + * @param User $user the user to verify the permissions against |
| 291 | + * @return mixed An array as returned by getUserPermissionsErrors or true |
| 292 | + * in case the user has proper permissions. |
| 293 | + */ |
| 294 | + public function verifyPermissions( $user ) { |
| 295 | + /** |
| 296 | + * If the image is protected, non-sysop users won't be able |
| 297 | + * to modify it by uploading a new revision. |
| 298 | + */ |
| 299 | + $nt = $this->getTitle(); |
| 300 | + if( is_null( $nt ) ) { |
| 301 | + return true; |
| 302 | + } |
| 303 | + $permErrors = $nt->getUserPermissionsErrors( 'edit', $user ); |
| 304 | + $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user ); |
| 305 | + $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) ); |
| 306 | + if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { |
| 307 | + $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); |
| 308 | + $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); |
| 309 | + return $permErrors; |
| 310 | + } |
| 311 | + return true; |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * Check for non fatal problems with the file |
| 316 | + * |
| 317 | + * @return array Array of warnings |
| 318 | + */ |
| 319 | + public function checkWarnings() { |
| 320 | + $warnings = array(); |
| 321 | + |
| 322 | + $localFile = $this->getLocalFile(); |
| 323 | + $filename = $localFile->getName(); |
| 324 | + $n = strrpos( $filename, '.' ); |
| 325 | + $partname = $n ? substr( $filename, 0, $n ) : $filename; |
| 326 | + |
| 327 | + /** |
| 328 | + * Check whether the resulting filename is different from the desired one, |
| 329 | + * but ignore things like ucfirst() and spaces/underscore things |
| 330 | + */ |
| 331 | + $comparableName = str_replace( ' ', '_', $this->mDesiredDestName ); |
| 332 | + $comparableName = Title::capitalize( $comparableName, NS_FILE ); |
| 333 | + |
| 334 | + if( $this->mDesiredDestName != $filename && $comparableName != $filename ) { |
| 335 | + $warnings['badfilename'] = $filename; |
| 336 | + } |
| 337 | + |
| 338 | + // Check whether the file extension is on the unwanted list |
| 339 | + global $wgCheckFileExtensions, $wgFileExtensions; |
| 340 | + if ( $wgCheckFileExtensions ) { |
| 341 | + if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) { |
| 342 | + $warnings['filetype-unwanted-type'] = $this->mFinalExtension; |
| 343 | + } |
| 344 | + } |
| 345 | + |
| 346 | + global $wgUploadSizeWarning; |
| 347 | + if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { |
| 348 | + $warnings['large-file'] = $wgUploadSizeWarning; |
| 349 | + } |
| 350 | + |
| 351 | + if ( $this->mFileSize == 0 ) { |
| 352 | + $warnings['emptyfile'] = true; |
| 353 | + } |
| 354 | + |
| 355 | + $exists = self::getExistsWarning( $localFile ); |
| 356 | + if( $exists !== false ) { |
| 357 | + $warnings['exists'] = $exists; |
| 358 | + } |
| 359 | + |
| 360 | + // Check dupes against existing files |
| 361 | + $hash = File::sha1Base36( $this->mTempPath ); |
| 362 | + $dupes = RepoGroup::singleton()->findBySha1( $hash ); |
| 363 | + $title = $this->getTitle(); |
| 364 | + // Remove all matches against self |
| 365 | + foreach ( $dupes as $key => $dupe ) { |
| 366 | + if( $title->equals( $dupe->getTitle() ) ) { |
| 367 | + unset( $dupes[$key] ); |
| 368 | + } |
| 369 | + } |
| 370 | + if( $dupes ) { |
| 371 | + $warnings['duplicate'] = $dupes; |
| 372 | + } |
| 373 | + |
| 374 | + // Check dupes against archives |
| 375 | + $archivedImage = new ArchivedFile( null, 0, "{$hash}.{$this->mFinalExtension}" ); |
| 376 | + if ( $archivedImage->getID() > 0 ) { |
| 377 | + $warnings['duplicate-archive'] = $archivedImage->getName(); |
| 378 | + } |
| 379 | + |
| 380 | + return $warnings; |
| 381 | + } |
| 382 | + |
| 383 | + /** |
| 384 | + * Really perform the upload. Stores the file in the local repo, watches |
| 385 | + * if necessary and runs the UploadComplete hook. |
| 386 | + * |
| 387 | + * @return mixed Status indicating the whether the upload succeeded. |
| 388 | + */ |
| 389 | + public function performUpload( $comment, $pageText, $watch, $user ) { |
| 390 | + wfDebug( "\n\n\performUpload: sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch ); |
| 391 | + $status = $this->getLocalFile()->upload( $this->mTempPath, $comment, $pageText, |
| 392 | + File::DELETE_SOURCE, $this->mFileProps, false, $user ); |
| 393 | + |
| 394 | + if( $status->isGood() && $watch ) { |
| 395 | + $user->addWatch( $this->getLocalFile()->getTitle() ); |
| 396 | + } |
| 397 | + |
| 398 | + if( $status->isGood() ) { |
| 399 | + wfRunHooks( 'UploadComplete', array( &$this ) ); |
| 400 | + } |
| 401 | + |
| 402 | + return $status; |
| 403 | + } |
| 404 | + |
| 405 | + /** |
| 406 | + * Returns the title of the file to be uploaded. Sets mTitleError in case |
| 407 | + * the name was illegal. |
| 408 | + * |
| 409 | + * @return Title The title of the file or null in case the name was illegal |
| 410 | + */ |
| 411 | + public function getTitle() { |
| 412 | + if ( $this->mTitle !== false ) { |
| 413 | + return $this->mTitle; |
| 414 | + } |
| 415 | + |
| 416 | + /** |
| 417 | + * Chop off any directories in the given filename. Then |
| 418 | + * filter out illegal characters, and try to make a legible name |
| 419 | + * out of it. We'll strip some silently that Title would die on. |
| 420 | + */ |
| 421 | + $basename = $this->mDesiredDestName; |
| 422 | + |
| 423 | + $this->mFilteredName = wfStripIllegalFilenameChars( $basename ); |
| 424 | + /* Normalize to title form before we do any further processing */ |
| 425 | + $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
| 426 | + if( is_null( $nt ) ) { |
| 427 | + $this->mTitleError = self::ILLEGAL_FILENAME; |
| 428 | + return $this->mTitle = null; |
| 429 | + } |
| 430 | + $this->mFilteredName = $nt->getDBkey(); |
| 431 | + |
| 432 | + /** |
| 433 | + * We'll want to blacklist against *any* 'extension', and use |
| 434 | + * only the final one for the whitelist. |
| 435 | + */ |
| 436 | + list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName ); |
| 437 | + |
| 438 | + if( count( $ext ) ) { |
| 439 | + $this->mFinalExtension = trim( $ext[count( $ext ) - 1] ); |
| 440 | + } else { |
| 441 | + $this->mFinalExtension = ''; |
| 442 | + } |
| 443 | + |
| 444 | + /* Don't allow users to override the blacklist (check file extension) */ |
| 445 | + global $wgCheckFileExtensions, $wgStrictFileExtensions; |
| 446 | + global $wgFileExtensions, $wgFileBlacklist; |
| 447 | + if ( $this->mFinalExtension == '' ) { |
| 448 | + $this->mTitleError = self::FILETYPE_MISSING; |
| 449 | + return $this->mTitle = null; |
| 450 | + } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || |
| 451 | + ( $wgCheckFileExtensions && $wgStrictFileExtensions && |
| 452 | + !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) { |
| 453 | + $this->mTitleError = self::FILETYPE_BADTYPE; |
| 454 | + return $this->mTitle = null; |
| 455 | + } |
| 456 | + |
| 457 | + # If there was more than one "extension", reassemble the base |
| 458 | + # filename to prevent bogus complaints about length |
| 459 | + if( count( $ext ) > 1 ) { |
| 460 | + for( $i = 0; $i < count( $ext ) - 1; $i++ ) { |
| 461 | + $partname .= '.' . $ext[$i]; |
| 462 | + } |
| 463 | + } |
| 464 | + |
| 465 | + if( strlen( $partname ) < 1 ) { |
| 466 | + $this->mTitleError = self::MIN_LENGTH_PARTNAME; |
| 467 | + return $this->mTitle = null; |
| 468 | + } |
| 469 | + |
| 470 | + $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
| 471 | + if( is_null( $nt ) ) { |
| 472 | + $this->mTitleError = self::ILLEGAL_FILENAME; |
| 473 | + return $this->mTitle = null; |
| 474 | + } |
| 475 | + return $this->mTitle = $nt; |
| 476 | + } |
| 477 | + |
| 478 | + /** |
| 479 | + * Return the local file and initializes if necessary. |
| 480 | + */ |
| 481 | + public function getLocalFile() { |
| 482 | + if( is_null( $this->mLocalFile ) ) { |
| 483 | + $nt = $this->getTitle(); |
| 484 | + $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt ); |
| 485 | + } |
| 486 | + return $this->mLocalFile; |
| 487 | + } |
| 488 | + |
| 489 | + /** |
| 490 | + * Stash a file in a temporary directory for later processing |
| 491 | + * after the user has confirmed it. |
| 492 | + * |
| 493 | + * If the user doesn't explicitly cancel or accept, these files |
| 494 | + * can accumulate in the temp directory. |
| 495 | + * |
| 496 | + * @param string $saveName - the destination filename |
| 497 | + * @param string $tempSrc - the source temporary file to save |
| 498 | + * @return string - full path the stashed file, or false on failure |
| 499 | + */ |
| 500 | + protected function saveTempUploadedFile( $saveName, $tempSrc ) { |
| 501 | + $repo = RepoGroup::singleton()->getLocalRepo(); |
| 502 | + $status = $repo->storeTemp( $saveName, $tempSrc ); |
| 503 | + return $status; |
| 504 | + } |
| 505 | + |
| 506 | + /** |
| 507 | + * Stash a file in a temporary directory for later processing, |
| 508 | + * and save the necessary descriptive info into the session. |
| 509 | + * Returns a key value which will be passed through a form |
| 510 | + * to pick up the path info on a later invocation. |
| 511 | + * |
| 512 | + * @return int Session key |
| 513 | + */ |
| 514 | + public function stashSession() { |
| 515 | + $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); |
| 516 | + if( !$status->isOK() ) { |
| 517 | + # Couldn't save the file. |
| 518 | + return false; |
| 519 | + } |
| 520 | + if( !isset( $_SESSION ) ) { |
| 521 | + session_start(); // start up the session (might have been previously closed to prevent php session locking) |
| 522 | + } |
| 523 | + $key = $this->getSessionKey(); |
| 524 | + $_SESSION['wsUploadData'][$key] = array( |
| 525 | + 'mTempPath' => $status->value, |
| 526 | + 'mFileSize' => $this->mFileSize, |
| 527 | + 'mFileProps' => $this->mFileProps, |
| 528 | + 'version' => self::SESSION_VERSION, |
| 529 | + ); |
| 530 | + return $key; |
| 531 | + } |
| 532 | + |
| 533 | + /** |
| 534 | + * Generate a random session key from stash in cases where we want to start an upload without much information |
| 535 | + */ |
| 536 | + protected function getSessionKey() { |
| 537 | + $key = mt_rand( 0, 0x7fffffff ); |
| 538 | + $_SESSION['wsUploadData'][$key] = array(); |
| 539 | + return $key; |
| 540 | + } |
| 541 | + |
| 542 | + /** |
| 543 | + * If we've modified the upload file we need to manually remove it |
| 544 | + * on exit to clean up. |
| 545 | + */ |
| 546 | + public function cleanupTempFile() { |
| 547 | + if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) { |
| 548 | + wfDebug( __METHOD__ . ": Removing temporary file {$this->mTempPath}\n" ); |
| 549 | + unlink( $this->mTempPath ); |
| 550 | + } |
| 551 | + } |
| 552 | + |
| 553 | + public function getTempPath() { |
| 554 | + return $this->mTempPath; |
| 555 | + } |
| 556 | + |
| 557 | + /** |
| 558 | + * Split a file into a base name and all dot-delimited 'extensions' |
| 559 | + * on the end. Some web server configurations will fall back to |
| 560 | + * earlier pseudo-'extensions' to determine type and execute |
| 561 | + * scripts, so the blacklist needs to check them all. |
| 562 | + * |
| 563 | + * @return array |
| 564 | + */ |
| 565 | + public static function splitExtensions( $filename ) { |
| 566 | + $bits = explode( '.', $filename ); |
| 567 | + $basename = array_shift( $bits ); |
| 568 | + return array( $basename, $bits ); |
| 569 | + } |
| 570 | + |
| 571 | + /** |
| 572 | + * Perform case-insensitive match against a list of file extensions. |
| 573 | + * Returns true if the extension is in the list. |
| 574 | + * |
| 575 | + * @param string $ext |
| 576 | + * @param array $list |
| 577 | + * @return bool |
| 578 | + */ |
| 579 | + public static function checkFileExtension( $ext, $list ) { |
| 580 | + return in_array( strtolower( $ext ), $list ); |
| 581 | + } |
| 582 | + |
| 583 | + /** |
| 584 | + * Perform case-insensitive match against a list of file extensions. |
| 585 | + * Returns true if any of the extensions are in the list. |
| 586 | + * |
| 587 | + * @param array $ext |
| 588 | + * @param array $list |
| 589 | + * @return bool |
| 590 | + */ |
| 591 | + public static function checkFileExtensionList( $ext, $list ) { |
| 592 | + foreach( $ext as $e ) { |
| 593 | + if( in_array( strtolower( $e ), $list ) ) { |
| 594 | + return true; |
| 595 | + } |
| 596 | + } |
| 597 | + return false; |
| 598 | + } |
| 599 | + |
| 600 | + /** |
| 601 | + * Checks if the mime type of the uploaded file matches the file extension. |
| 602 | + * |
| 603 | + * @param string $mime the mime type of the uploaded file |
| 604 | + * @param string $extension The filename extension that the file is to be served with |
| 605 | + * @return bool |
| 606 | + */ |
| 607 | + public static function verifyExtension( $mime, $extension ) { |
| 608 | + $magic = MimeMagic::singleton(); |
| 609 | + |
| 610 | + if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) |
| 611 | + if ( !$magic->isRecognizableExtension( $extension ) ) { |
| 612 | + wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " . |
| 613 | + "unrecognized extension '$extension', can't verify\n" ); |
| 614 | + return true; |
| 615 | + } else { |
| 616 | + wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; ". |
| 617 | + "recognized extension '$extension', so probably invalid file\n" ); |
| 618 | + return false; |
| 619 | + } |
| 620 | + |
| 621 | + $match = $magic->isMatchingExtension( $extension, $mime ); |
| 622 | + |
| 623 | + if ( $match === null ) { |
| 624 | + wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" ); |
| 625 | + return true; |
| 626 | + } elseif( $match === true ) { |
| 627 | + wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" ); |
| 628 | + |
| 629 | + #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! |
| 630 | + return true; |
| 631 | + |
| 632 | + } else { |
| 633 | + wfDebug( __METHOD__ . ": mime type $mime mismatches file extension $extension, rejecting file\n" ); |
| 634 | + return false; |
| 635 | + } |
| 636 | + } |
| 637 | + |
| 638 | + /** |
| 639 | + * Heuristic for detecting files that *could* contain JavaScript instructions or |
| 640 | + * things that may look like HTML to a browser and are thus |
| 641 | + * potentially harmful. The present implementation will produce false |
| 642 | + * positives in some situations. |
| 643 | + * |
| 644 | + * @param string $file Pathname to the temporary upload file |
| 645 | + * @param string $mime The mime type of the file |
| 646 | + * @param string $extension The extension of the file |
| 647 | + * @return bool true if the file contains something looking like embedded scripts |
| 648 | + */ |
| 649 | + public static function detectScript( $file, $mime, $extension ) { |
| 650 | + global $wgAllowTitlesInSVG; |
| 651 | + |
| 652 | + # ugly hack: for text files, always look at the entire file. |
| 653 | + # For binary field, just check the first K. |
| 654 | + |
| 655 | + if( strpos( $mime,'text/' ) === 0 ) { |
| 656 | + $chunk = file_get_contents( $file ); |
| 657 | + } else { |
| 658 | + $fp = fopen( $file, 'rb' ); |
| 659 | + $chunk = fread( $fp, 1024 ); |
| 660 | + fclose( $fp ); |
| 661 | + } |
| 662 | + |
| 663 | + $chunk = strtolower( $chunk ); |
| 664 | + |
| 665 | + if( !$chunk ) { |
| 666 | + return false; |
| 667 | + } |
| 668 | + |
| 669 | + # decode from UTF-16 if needed (could be used for obfuscation). |
| 670 | + if( substr( $chunk, 0, 2 ) == "\xfe\xff" ) { |
| 671 | + $enc = 'UTF-16BE'; |
| 672 | + } elseif( substr( $chunk, 0, 2 ) == "\xff\xfe" ) { |
| 673 | + $enc = 'UTF-16LE'; |
| 674 | + } else { |
| 675 | + $enc = null; |
| 676 | + } |
| 677 | + |
| 678 | + if( $enc ) { |
| 679 | + $chunk = iconv( $enc, "ASCII//IGNORE", $chunk ); |
| 680 | + } |
| 681 | + |
| 682 | + $chunk = trim( $chunk ); |
| 683 | + |
| 684 | + # FIXME: convert from UTF-16 if necessarry! |
| 685 | + wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" ); |
| 686 | + |
| 687 | + # check for HTML doctype |
| 688 | + if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) { |
| 689 | + return true; |
| 690 | + } |
| 691 | + |
| 692 | + /** |
| 693 | + * Internet Explorer for Windows performs some really stupid file type |
| 694 | + * autodetection which can cause it to interpret valid image files as HTML |
| 695 | + * and potentially execute JavaScript, creating a cross-site scripting |
| 696 | + * attack vectors. |
| 697 | + * |
| 698 | + * Apple's Safari browser also performs some unsafe file type autodetection |
| 699 | + * which can cause legitimate files to be interpreted as HTML if the |
| 700 | + * web server is not correctly configured to send the right content-type |
| 701 | + * (or if you're really uploading plain text and octet streams!) |
| 702 | + * |
| 703 | + * Returns true if IE is likely to mistake the given file for HTML. |
| 704 | + * Also returns true if Safari would mistake the given file for HTML |
| 705 | + * when served with a generic content-type. |
| 706 | + */ |
| 707 | + $tags = array( |
| 708 | + '<a href', |
| 709 | + '<body', |
| 710 | + '<head', |
| 711 | + '<html', #also in safari |
| 712 | + '<img', |
| 713 | + '<pre', |
| 714 | + '<script', #also in safari |
| 715 | + '<table' |
| 716 | + ); |
| 717 | + |
| 718 | + if( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { |
| 719 | + $tags[] = '<title'; |
| 720 | + } |
| 721 | + |
| 722 | + foreach( $tags as $tag ) { |
| 723 | + if( false !== strpos( $chunk, $tag ) ) { |
| 724 | + return true; |
| 725 | + } |
| 726 | + } |
| 727 | + |
| 728 | + /* |
| 729 | + * look for JavaScript |
| 730 | + */ |
| 731 | + |
| 732 | + # resolve entity-refs to look at attributes. may be harsh on big files... cache result? |
| 733 | + $chunk = Sanitizer::decodeCharReferences( $chunk ); |
| 734 | + |
| 735 | + # look for script-types |
| 736 | + if( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) { |
| 737 | + return true; |
| 738 | + } |
| 739 | + |
| 740 | + # look for html-style script-urls |
| 741 | + if( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { |
| 742 | + return true; |
| 743 | + } |
| 744 | + |
| 745 | + # look for css-style script-urls |
| 746 | + if( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { |
| 747 | + return true; |
| 748 | + } |
| 749 | + |
| 750 | + wfDebug( __METHOD__ . ": no scripts found\n" ); |
| 751 | + return false; |
| 752 | + } |
| 753 | + |
| 754 | + protected function detectScriptInSvg( $filename ) { |
| 755 | + $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) ); |
| 756 | + return $check->filterMatch; |
| 757 | + } |
| 758 | + |
| 759 | + /** |
| 760 | + * @todo Replace this with a whitelist filter! |
| 761 | + */ |
| 762 | + public function checkSvgScriptCallback( $element, $attribs ) { |
| 763 | + $stripped = $this->stripXmlNamespace( $element ); |
| 764 | + |
| 765 | + if( $stripped == 'script' ) { |
| 766 | + wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" ); |
| 767 | + return true; |
| 768 | + } |
| 769 | + |
| 770 | + foreach( $attribs as $attrib => $value ) { |
| 771 | + $stripped = $this->stripXmlNamespace( $attrib ); |
| 772 | + if( substr( $stripped, 0, 2 ) == 'on' ) { |
| 773 | + wfDebug( __METHOD__ . ": Found script attribute '$attrib'='value' in uploaded file.\n" ); |
| 774 | + return true; |
| 775 | + } |
| 776 | + if( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) { |
| 777 | + wfDebug( __METHOD__ . ": Found script href attribute '$attrib'='$value' in uploaded file.\n" ); |
| 778 | + return true; |
| 779 | + } |
| 780 | + } |
| 781 | + } |
| 782 | + |
| 783 | + private function stripXmlNamespace( $name ) { |
| 784 | + // 'http://www.w3.org/2000/svg:script' -> 'script' |
| 785 | + $parts = explode( ':', strtolower( $name ) ); |
| 786 | + return array_pop( $parts ); |
| 787 | + } |
| 788 | + |
| 789 | + /** |
| 790 | + * Generic wrapper function for a virus scanner program. |
| 791 | + * This relies on the $wgAntivirus and $wgAntivirusSetup variables. |
| 792 | + * $wgAntivirusRequired may be used to deny upload if the scan fails. |
| 793 | + * |
| 794 | + * @param string $file Pathname to the temporary upload file |
| 795 | + * @return mixed false if not virus is found, NULL if the scan fails or is disabled, |
| 796 | + * or a string containing feedback from the virus scanner if a virus was found. |
| 797 | + * If textual feedback is missing but a virus was found, this function returns true. |
| 798 | + */ |
| 799 | + public static function detectVirus( $file ) { |
| 800 | + global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; |
| 801 | + |
| 802 | + if ( !$wgAntivirus ) { |
| 803 | + wfDebug( __METHOD__ . ": virus scanner disabled\n" ); |
| 804 | + return null; |
| 805 | + } |
| 806 | + |
| 807 | + if ( !$wgAntivirusSetup[$wgAntivirus] ) { |
| 808 | + wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" ); |
| 809 | + $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1</div>", array( 'virus-badscanner', $wgAntivirus ) ); |
| 810 | + return wfMsg( 'virus-unknownscanner' ) . " $wgAntivirus"; |
| 811 | + } |
| 812 | + |
| 813 | + # look up scanner configuration |
| 814 | + $command = $wgAntivirusSetup[$wgAntivirus]['command']; |
| 815 | + $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap']; |
| 816 | + $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ) ? |
| 817 | + $wgAntivirusSetup[$wgAntivirus]['messagepattern'] : null; |
| 818 | + |
| 819 | + if ( strpos( $command, "%f" ) === false ) { |
| 820 | + # simple pattern: append file to scan |
| 821 | + $command .= " " . wfEscapeShellArg( $file ); |
| 822 | + } else { |
| 823 | + # complex pattern: replace "%f" with file to scan |
| 824 | + $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); |
| 825 | + } |
| 826 | + |
| 827 | + wfDebug( __METHOD__ . ": running virus scan: $command \n" ); |
| 828 | + |
| 829 | + # execute virus scanner |
| 830 | + $exitCode = false; |
| 831 | + |
| 832 | + # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. |
| 833 | + # that does not seem to be worth the pain. |
| 834 | + # Ask me (Duesentrieb) about it if it's ever needed. |
| 835 | + $output = wfShellExec( "$command 2>&1", $exitCode ); |
| 836 | + |
| 837 | + # map exit code to AV_xxx constants. |
| 838 | + $mappedCode = $exitCode; |
| 839 | + if ( $exitCodeMap ) { |
| 840 | + if ( isset( $exitCodeMap[$exitCode] ) ) { |
| 841 | + $mappedCode = $exitCodeMap[$exitCode]; |
| 842 | + } elseif ( isset( $exitCodeMap["*"] ) ) { |
| 843 | + $mappedCode = $exitCodeMap["*"]; |
| 844 | + } |
| 845 | + } |
| 846 | + |
| 847 | + if ( $mappedCode === AV_SCAN_FAILED ) { |
| 848 | + # scan failed (code was mapped to false by $exitCodeMap) |
| 849 | + wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" ); |
| 850 | + |
| 851 | + if ( $wgAntivirusRequired ) { |
| 852 | + return wfMsg( 'virus-scanfailed', array( $exitCode ) ); |
| 853 | + } else { |
| 854 | + return null; |
| 855 | + } |
| 856 | + } elseif ( $mappedCode === AV_SCAN_ABORTED ) { |
| 857 | + # scan failed because filetype is unknown (probably imune) |
| 858 | + wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" ); |
| 859 | + return null; |
| 860 | + } elseif ( $mappedCode === AV_NO_VIRUS ) { |
| 861 | + # no virus found |
| 862 | + wfDebug( __METHOD__ . ": file passed virus scan.\n" ); |
| 863 | + return false; |
| 864 | + } else { |
| 865 | + $output = trim( $output ); |
| 866 | + |
| 867 | + if ( !$output ) { |
| 868 | + $output = true; #if there's no output, return true |
| 869 | + } elseif ( $msgPattern ) { |
| 870 | + $groups = array(); |
| 871 | + if ( preg_match( $msgPattern, $output, $groups ) ) { |
| 872 | + if ( $groups[1] ) { |
| 873 | + $output = $groups[1]; |
| 874 | + } |
| 875 | + } |
| 876 | + } |
| 877 | + |
| 878 | + wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" ); |
| 879 | + return $output; |
| 880 | + } |
| 881 | + } |
| 882 | + |
| 883 | + /** |
| 884 | + * Check if the temporary file is MacBinary-encoded, as some uploads |
| 885 | + * from Internet Explorer on Mac OS Classic and Mac OS X will be. |
| 886 | + * If so, the data fork will be extracted to a second temporary file, |
| 887 | + * which will then be checked for validity and either kept or discarded. |
| 888 | + */ |
| 889 | + private function checkMacBinary() { |
| 890 | + $macbin = new MacBinary( $this->mTempPath ); |
| 891 | + if( $macbin->isValid() ) { |
| 892 | + $dataFile = tempnam( wfTempDir(), 'WikiMacBinary' ); |
| 893 | + $dataHandle = fopen( $dataFile, 'wb' ); |
| 894 | + |
| 895 | + wfDebug( __METHOD__ . ": Extracting MacBinary data fork to $dataFile\n" ); |
| 896 | + $macbin->extractData( $dataHandle ); |
| 897 | + |
| 898 | + $this->mTempPath = $dataFile; |
| 899 | + $this->mFileSize = $macbin->dataForkLength(); |
| 900 | + |
| 901 | + // We'll have to manually remove the new file if it's not kept. |
| 902 | + $this->mRemoveTempFile = true; |
| 903 | + } |
| 904 | + $macbin->close(); |
| 905 | + } |
| 906 | + |
| 907 | + /** |
| 908 | + * Check if there's an overwrite conflict and, if so, if restrictions |
| 909 | + * forbid this user from performing the upload. |
| 910 | + * |
| 911 | + * @return mixed true on success, error string on failure |
| 912 | + */ |
| 913 | + private function checkOverwrite() { |
| 914 | + global $wgUser; |
| 915 | + // First check whether the local file can be overwritten |
| 916 | + $file = $this->getLocalFile(); |
| 917 | + if( $file->exists() ) { |
| 918 | + if( !self::userCanReUpload( $wgUser, $file ) ) { |
| 919 | + return 'fileexists-forbidden'; |
| 920 | + } else { |
| 921 | + return true; |
| 922 | + } |
| 923 | + } |
| 924 | + |
| 925 | + /* Check shared conflicts: if the local file does not exist, but |
| 926 | + * wfFindFile finds a file, it exists in a shared repository. |
| 927 | + */ |
| 928 | + $file = wfFindFile( $this->getTitle() ); |
| 929 | + if ( $file && !$wgUser->isAllowed( 'reupload-shared' ) ) { |
| 930 | + return 'fileexists-shared-forbidden'; |
| 931 | + } |
| 932 | + |
| 933 | + return true; |
| 934 | + } |
| 935 | + |
| 936 | + /** |
| 937 | + * Check if a user is the last uploader |
| 938 | + * |
| 939 | + * @param User $user |
| 940 | + * @param string $img, image name |
| 941 | + * @return bool |
| 942 | + */ |
| 943 | + public static function userCanReUpload( User $user, $img ) { |
| 944 | + if( $user->isAllowed( 'reupload' ) ) { |
| 945 | + return true; // non-conditional |
| 946 | + } |
| 947 | + if( !$user->isAllowed( 'reupload-own' ) ) { |
| 948 | + return false; |
| 949 | + } |
| 950 | + if( is_string( $img ) ) { |
| 951 | + $img = wfLocalFile( $img ); |
| 952 | + } |
| 953 | + if ( !( $img instanceof LocalFile ) ) { |
| 954 | + return false; |
| 955 | + } |
| 956 | + |
| 957 | + return $user->getId() == $img->getUser( 'id' ); |
| 958 | + } |
| 959 | + |
| 960 | + /** |
| 961 | + * Helper function that does various existence checks for a file. |
| 962 | + * The following checks are performed: |
| 963 | + * - The file exists |
| 964 | + * - Article with the same name as the file exists |
| 965 | + * - File exists with normalized extension |
| 966 | + * - The file looks like a thumbnail and the original exists |
| 967 | + * |
| 968 | + * @param File $file The file to check |
| 969 | + * @return mixed False if the file does not exists, else an array |
| 970 | + */ |
| 971 | + public static function getExistsWarning( $file ) { |
| 972 | + if( $file->exists() ) { |
| 973 | + return array( 'warning' => 'exists', 'file' => $file ); |
| 974 | + } |
| 975 | + |
| 976 | + if( $file->getTitle()->getArticleID() ) { |
| 977 | + return array( 'warning' => 'page-exists', 'file' => $file ); |
| 978 | + } |
| 979 | + |
| 980 | + if ( $file->wasDeleted() && !$file->exists() ) { |
| 981 | + return array( 'warning' => 'was-deleted', 'file' => $file ); |
| 982 | + } |
| 983 | + |
| 984 | + if( strpos( $file->getName(), '.' ) == false ) { |
| 985 | + $partname = $file->getName(); |
| 986 | + $extension = ''; |
| 987 | + } else { |
| 988 | + $n = strrpos( $file->getName(), '.' ); |
| 989 | + $extension = substr( $file->getName(), $n + 1 ); |
| 990 | + $partname = substr( $file->getName(), 0, $n ); |
| 991 | + } |
| 992 | + $normalizedExtension = File::normalizeExtension( $extension ); |
| 993 | + |
| 994 | + if ( $normalizedExtension != $extension ) { |
| 995 | + // We're not using the normalized form of the extension. |
| 996 | + // Normal form is lowercase, using most common of alternate |
| 997 | + // extensions (eg 'jpg' rather than 'JPEG'). |
| 998 | + // |
| 999 | + // Check for another file using the normalized form... |
| 1000 | + $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" ); |
| 1001 | + $file_lc = wfLocalFile( $nt_lc ); |
| 1002 | + |
| 1003 | + if( $file_lc->exists() ) { |
| 1004 | + return array( |
| 1005 | + 'warning' => 'exists-normalized', |
| 1006 | + 'file' => $file, |
| 1007 | + 'normalizedFile' => $file_lc |
| 1008 | + ); |
| 1009 | + } |
| 1010 | + } |
| 1011 | + |
| 1012 | + if ( self::isThumbName( $file->getName() ) ) { |
| 1013 | + # Check for filenames like 50px- or 180px-, these are mostly thumbnails |
| 1014 | + $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $extension, NS_FILE ); |
| 1015 | + $file_thb = wfLocalFile( $nt_thb ); |
| 1016 | + if( $file_thb->exists() ) { |
| 1017 | + return array( |
| 1018 | + 'warning' => 'thumb', |
| 1019 | + 'file' => $file, |
| 1020 | + 'thumbFile' => $file_thb |
| 1021 | + ); |
| 1022 | + } else { |
| 1023 | + // File does not exist, but we just don't like the name |
| 1024 | + return array( |
| 1025 | + 'warning' => 'thumb-name', |
| 1026 | + 'file' => $file, |
| 1027 | + 'thumbFile' => $file_thb |
| 1028 | + ); |
| 1029 | + } |
| 1030 | + } |
| 1031 | + |
| 1032 | + |
| 1033 | + foreach( self::getFilenamePrefixBlacklist() as $prefix ) { |
| 1034 | + if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { |
| 1035 | + return array( |
| 1036 | + 'warning' => 'bad-prefix', |
| 1037 | + 'file' => $file, |
| 1038 | + 'prefix' => $prefix |
| 1039 | + ); |
| 1040 | + } |
| 1041 | + } |
| 1042 | + |
| 1043 | + return false; |
| 1044 | + } |
| 1045 | + |
| 1046 | + /** |
| 1047 | + * Helper function that checks whether the filename looks like a thumbnail |
| 1048 | + */ |
| 1049 | + public static function isThumbName( $filename ) { |
| 1050 | + $n = strrpos( $filename, '.' ); |
| 1051 | + $partname = $n ? substr( $filename, 0, $n ) : $filename; |
| 1052 | + return ( |
| 1053 | + substr( $partname , 3, 3 ) == 'px-' || |
| 1054 | + substr( $partname , 2, 3 ) == 'px-' |
| 1055 | + ) && |
| 1056 | + preg_match( "/[0-9]{2}/" , substr( $partname , 0, 2 ) ); |
| 1057 | + } |
| 1058 | + |
| 1059 | + /** |
| 1060 | + * Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]] |
| 1061 | + * |
| 1062 | + * @return array list of prefixes |
| 1063 | + */ |
| 1064 | + public static function getFilenamePrefixBlacklist() { |
| 1065 | + $blacklist = array(); |
| 1066 | + $message = wfMsgForContent( 'filename-prefix-blacklist' ); |
| 1067 | + if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) { |
| 1068 | + $lines = explode( "\n", $message ); |
| 1069 | + foreach( $lines as $line ) { |
| 1070 | + // Remove comment lines |
| 1071 | + $comment = substr( trim( $line ), 0, 1 ); |
| 1072 | + if ( $comment == '#' || $comment == '' ) { |
| 1073 | + continue; |
| 1074 | + } |
| 1075 | + // Remove additional comments after a prefix |
| 1076 | + $comment = strpos( $line, '#' ); |
| 1077 | + if ( $comment > 0 ) { |
| 1078 | + $line = substr( $line, 0, $comment-1 ); |
| 1079 | + } |
| 1080 | + $blacklist[] = trim( $line ); |
| 1081 | + } |
| 1082 | + } |
| 1083 | + return $blacklist; |
| 1084 | + } |
| 1085 | + |
| 1086 | + public function getImageInfo( $result ) { |
| 1087 | + $file = $this->getLocalFile(); |
| 1088 | + $imParam = ApiQueryImageInfo::getPropertyNames(); |
| 1089 | + return ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result ); |
| 1090 | + } |
| 1091 | + |
| 1092 | +} |
Property changes on: trunk/extensions/DSMW/api/upload/UploadBase.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 1093 | + native |
Index: trunk/extensions/DSMW/api/upload/UploadFromFile.php |
— | — | @@ -0,0 +1,32 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * @file |
| 5 | + * @ingroup upload |
| 6 | + * |
| 7 | + * @author Bryan Tong Minh |
| 8 | + * |
| 9 | + * Implements regular file uploads |
| 10 | + */ |
| 11 | +class UploadFromFile extends UploadBase { |
| 12 | + |
| 13 | + |
| 14 | + function initializeFromRequest( &$request ) { |
| 15 | + $desiredDestName = $request->getText( 'wpDestFile' ); |
| 16 | + if( !$desiredDestName ) |
| 17 | + $desiredDestName = $request->getText( 'wpUploadFile' ); |
| 18 | + return $this->initializePathInfo( |
| 19 | + $desiredDestName, |
| 20 | + $request->getFileTempName( 'wpUploadFile' ), |
| 21 | + $request->getFileSize( 'wpUploadFile' ) |
| 22 | + ); |
| 23 | + } |
| 24 | + /** |
| 25 | + * Entry point for upload from file. |
| 26 | + */ |
| 27 | + function initialize( $name, $tempPath, $fileSize ) { |
| 28 | + return $this->initializePathInfo( $name, $tempPath, $fileSize ); |
| 29 | + } |
| 30 | + static function isValidRequest( $request ) { |
| 31 | + return (bool)$request->getFileTempName( 'wpUploadFile' ); |
| 32 | + } |
| 33 | +} |
Property changes on: trunk/extensions/DSMW/api/upload/UploadFromFile.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 34 | + native |