Index: trunk/phase3/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php |
— | — | @@ -0,0 +1,289 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/* |
| 5 | + * RandomImageGenerator -- does what it says on the tin. |
| 6 | + * Requires Imagick, the ImageMagick library for PHP, or the command line equivalent (usually 'convert'). |
| 7 | + * |
| 8 | + * Because MediaWiki tests the uniqueness of media upload content, and filenames, it is sometimes useful to generate |
| 9 | + * files that are guaranteed (or at least very likely) to be unique in both those ways. |
| 10 | + * This generates a number of filenames with random names and random content (colored circles) |
| 11 | + * |
| 12 | + * It is also useful to have fresh content because our tests currently run in a "destructive" mode, and don't create a fresh new wiki for each |
| 13 | + * test run. |
| 14 | + * Consequently, if we just had a few static files we kept re-uploading, we'd get lots of warnings about matching content or filenames, |
| 15 | + * and even if we deleted those files, we'd get warnings about archived files. |
| 16 | + * |
| 17 | + * This can also be used with a cronjob to generate random files all the time -- I use it to have a constant, never ending supply when I'm |
| 18 | + * testing interactively. |
| 19 | + * |
| 20 | + * @file |
| 21 | + * @author Neil Kandalgaonkar <neilk@wikimedia.org> |
| 22 | + */ |
| 23 | + |
| 24 | +/** |
| 25 | + * RandomImageGenerator: does what it says on the tin. |
| 26 | + * Can fetch a random image, or also write a number of them to disk with random filenames. |
| 27 | + */ |
| 28 | +class RandomImageGenerator { |
| 29 | + |
| 30 | + private $dictionaryFile; |
| 31 | + private $minWidth = 400; |
| 32 | + private $maxWidth = 800; |
| 33 | + private $minHeight = 400; |
| 34 | + private $maxHeight = 800; |
| 35 | + private $circlesToDraw = 5; |
| 36 | + private $imageWriteMethod; |
| 37 | + |
| 38 | + public function __construct( $options ) { |
| 39 | + global $wgUseImageMagick, $wgImageMagickConvertCommand; |
| 40 | + foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'circlesToDraw' ) as $property ) { |
| 41 | + if ( isset( $options[$property] ) ) { |
| 42 | + $this->$property = $options[$property]; |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + // find the dictionary file, to generate random names |
| 47 | + if ( !isset( $this->dictionaryFile ) ) { |
| 48 | + foreach ( array( '/usr/share/dict/words', '/usr/dict/words' ) as $dictionaryFile ) { |
| 49 | + if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { |
| 50 | + $this->dictionaryFile = $dictionaryFile; |
| 51 | + break; |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + if ( !isset( $this->dictionaryFile ) ) { |
| 56 | + throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" ); |
| 57 | + } |
| 58 | + |
| 59 | + // figure out how to write images |
| 60 | + if ( class_exists( 'Imagick' ) ) { |
| 61 | + $this->imageWriteMethod = 'writeImageWithApi'; |
| 62 | + } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { |
| 63 | + $this->imageWriteMethod = 'writeImageWithCommandLine'; |
| 64 | + } else { |
| 65 | + throw new Exception( "RandomImageGenerator: could not find a suitable method to write images" ); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Writes random images with random filenames to disk in the directory you specify, or current working directory |
| 71 | + * |
| 72 | + * @param {Integer} number of filenames to write |
| 73 | + * @param {String} format, optional, must be understood by ImageMagick, such as 'jpg' or 'gif' |
| 74 | + * @param {String} directory, optional (will default to current working directory) |
| 75 | + * @return {Array} filenames we just wrote |
| 76 | + */ |
| 77 | + function writeImages( $number, $format = 'jpg', $dir = null ) { |
| 78 | + $filenames = $this->getRandomFilenames( $number, $format, $dir ); |
| 79 | + foreach( $filenames as $filename ) { |
| 80 | + $this->{$this->imageWriteMethod}( $this->getImageSpec(), $format, $filename ); |
| 81 | + } |
| 82 | + return $filenames; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Return a number of randomly-generated filenames |
| 87 | + * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg |
| 88 | + * |
| 89 | + * @param {Integer} number of filenames to generate |
| 90 | + * @param {String} extension, optional, defaults to 'jpg' |
| 91 | + * @param {String} directory, optional, defaults to current working directory |
| 92 | + * @return {Array} of filenames |
| 93 | + */ |
| 94 | + private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) { |
| 95 | + if ( is_null( $dir ) ) { |
| 96 | + $dir = getcwd(); |
| 97 | + } |
| 98 | + $filenames = array(); |
| 99 | + foreach( $this->getRandomWordPairs( $number ) as $pair ) { |
| 100 | + $basename = $pair[0] . '_' . $pair[1]; |
| 101 | + if ( !is_null( $extension ) ) { |
| 102 | + $basename .= '.' . $extension; |
| 103 | + } |
| 104 | + $basename = preg_replace( '/\s+/', '', $basename ); |
| 105 | + $filenames[] = "$dir/$basename"; |
| 106 | + } |
| 107 | + |
| 108 | + return $filenames; |
| 109 | + |
| 110 | + } |
| 111 | + |
| 112 | + |
| 113 | + /** |
| 114 | + * Generate data representing an image of random size (within limits), |
| 115 | + * consisting of randomly colored and sized circles against a random background color |
| 116 | + * (This data is used in the writeImage* methods). |
| 117 | + * @return {Mixed} |
| 118 | + */ |
| 119 | + public function getImageSpec() { |
| 120 | + $spec = array(); |
| 121 | + |
| 122 | + $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth ); |
| 123 | + $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight ); |
| 124 | + $spec['fill'] = $this->getRandomColor(); |
| 125 | + |
| 126 | + $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) ); |
| 127 | + |
| 128 | + $draws = array(); |
| 129 | + for ( $i = 0; $i <= $this->circlesToDraw; $i++ ) { |
| 130 | + $radius = mt_rand( 0, $diagonalLength / 4 ); |
| 131 | + $originX = mt_rand( -1 * $radius, $spec['width'] + $radius ); |
| 132 | + $originY = mt_rand( -1 * $radius, $spec['height'] + $radius ); |
| 133 | + $perimeterX = $originX + $radius; |
| 134 | + $perimeterY = $originY + $radius; |
| 135 | + |
| 136 | + $draw = array(); |
| 137 | + $draw['fill'] = $this->getRandomColor(); |
| 138 | + $draw['circle'] = array( |
| 139 | + 'originX' => $originX, |
| 140 | + 'originY' => $originY, |
| 141 | + 'perimeterX' => $perimeterX, |
| 142 | + 'perimeterY' => $perimeterY |
| 143 | + ); |
| 144 | + $draws[] = $draw; |
| 145 | + |
| 146 | + } |
| 147 | + |
| 148 | + $spec['draws'] = $draws; |
| 149 | + |
| 150 | + return $spec; |
| 151 | + } |
| 152 | + |
| 153 | + |
| 154 | + /** |
| 155 | + * Based on an image specification, write such an image to disk, using Imagick PHP extension |
| 156 | + * @param $spec: spec describing background and circles to draw |
| 157 | + * @param $format: file format to write |
| 158 | + * @param $filename: filename to write to |
| 159 | + */ |
| 160 | + public function writeImageWithApi( $spec, $format, $filename ) { |
| 161 | + $image = new Imagick(); |
| 162 | + $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); |
| 163 | + |
| 164 | + foreach ( $spec['draws'] as $drawSpec ) { |
| 165 | + $draw = new ImagickDraw(); |
| 166 | + $draw->setFillColor( $drawSpec['fill'] ); |
| 167 | + $circle = $drawSpec['circle']; |
| 168 | + $draw->circle( $circle['originX'], $circle['originY'], $circle['perimeterX'], $circle['perimeterY'] ); |
| 169 | + $image->drawImage( $draw ); |
| 170 | + } |
| 171 | + |
| 172 | + $image->setImageFormat( $format ); |
| 173 | + $image->writeImage( $filename ); |
| 174 | + } |
| 175 | + |
| 176 | + |
| 177 | + /** |
| 178 | + * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert'). |
| 179 | + * |
| 180 | + * Sample command line: |
| 181 | + * $ convert -size 100x60 xc:rgb(90,87,45) \ |
| 182 | + * -draw 'fill rgb(12,34,56) circle 41,39 44,57' \ |
| 183 | + * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \ |
| 184 | + * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png |
| 185 | + * |
| 186 | + * @param $spec: spec describing background and circles to draw |
| 187 | + * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi) |
| 188 | + * @param $filename: filename to write to |
| 189 | + */ |
| 190 | + public function writeImageWithCommandLine( $spec, $format, $filename ) { |
| 191 | + global $wgImageMagickConvertCommand; |
| 192 | + $args = array(); |
| 193 | + $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] ); |
| 194 | + $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] ); |
| 195 | + foreach( $spec['draws'] as $draw ) { |
| 196 | + $fill = $draw['fill']; |
| 197 | + $originX = $draw['circle']['originX']; |
| 198 | + $originY = $draw['circle']['originY']; |
| 199 | + $perimeterX = $draw['circle']['perimeterX']; |
| 200 | + $perimeterY = $draw['circle']['perimeterY']; |
| 201 | + $drawCommand = "fill $fill circle $originX,$originY $perimeterX,$perimeterY"; |
| 202 | + $args[] = '-draw ' . wfEscapeShellArg( $drawCommand ); |
| 203 | + } |
| 204 | + $args[] = $filename; |
| 205 | + |
| 206 | + $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args ); |
| 207 | + $output = wfShellExec( $command, $retval ); |
| 208 | + return ( $retval === 0 ); |
| 209 | + } |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | + /** |
| 214 | + * Generate a string of random colors for ImageMagick, like "rgb(12, 37, 98)" |
| 215 | + * |
| 216 | + * @return {String} |
| 217 | + */ |
| 218 | + public function getRandomColor() { |
| 219 | + $components = array(); |
| 220 | + for ($i = 0; $i <= 2; $i++ ) { |
| 221 | + $components[] = mt_rand( 0, 255 ); |
| 222 | + } |
| 223 | + return 'rgb(' . join(', ', $components) . ')'; |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) ); |
| 228 | + * |
| 229 | + * @param {Integer} number of pairs |
| 230 | + * @return {Array} of two-element arrays |
| 231 | + */ |
| 232 | + private function getRandomWordPairs( $number ) { |
| 233 | + $lines = $this->getRandomLines( $number * 2 ); |
| 234 | + // construct pairs of words |
| 235 | + $pairs = array(); |
| 236 | + $count = count( $lines ); |
| 237 | + for( $i = 0; $i < $count; $i += 2 ) { |
| 238 | + $pairs[] = array( $lines[$i], $lines[$i+1] ); |
| 239 | + } |
| 240 | + return $pairs; |
| 241 | + } |
| 242 | + |
| 243 | + |
| 244 | + /** |
| 245 | + * Return N random lines from a file |
| 246 | + * |
| 247 | + * Will throw exception if the file could not be read or if it had fewer lines than requested. |
| 248 | + * |
| 249 | + * @param {Integer} number of lines desired |
| 250 | + * @string {String} path to file |
| 251 | + * @return {Array} of exactly n elements, drawn randomly from lines the file |
| 252 | + */ |
| 253 | + private function getRandomLines( $number_desired ) { |
| 254 | + $filepath = $this->dictionaryFile; |
| 255 | + |
| 256 | + // initialize array of lines |
| 257 | + $lines = array(); |
| 258 | + for ( $i = 0; $i < $number_desired; $i++ ) { |
| 259 | + $lines[] = null; |
| 260 | + } |
| 261 | + |
| 262 | + /* |
| 263 | + * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of |
| 264 | + * a fixed-size array of lines, less and less frequently as it reads the file. |
| 265 | + */ |
| 266 | + $fh = fopen( $filepath, "r" ); |
| 267 | + if ( !$fh ) { |
| 268 | + throw new Exception( "couldn't open $filepath" ); |
| 269 | + } |
| 270 | + $line_number = 0; |
| 271 | + $max_index = $number_desired - 1; |
| 272 | + while( !feof( $fh ) ) { |
| 273 | + $line = fgets( $fh ); |
| 274 | + if ( $line !== false ) { |
| 275 | + $line_number++; |
| 276 | + $line = trim( $line ); |
| 277 | + if ( mt_rand( 0, $line_number ) <= $max_index ) { |
| 278 | + $lines[ mt_rand( 0, $max_index ) ] = $line; |
| 279 | + } |
| 280 | + } |
| 281 | + } |
| 282 | + fclose( $fh ); |
| 283 | + if ( $line_number < $number_desired ) { |
| 284 | + throw new Exception( "not enough lines in $filepath" ); |
| 285 | + } |
| 286 | + |
| 287 | + return $lines; |
| 288 | + } |
| 289 | + |
| 290 | +} |
Property changes on: trunk/phase3/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1 | 291 | Merged /trunk/phase3/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php:r73549-75058,75060-75821 |
2 | 292 | Merged /branches/phpunit-restructure/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php:r72257-72560 |
Added: svn:eol-style |
3 | 293 | + native |
Index: trunk/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php |
— | — | @@ -0,0 +1,645 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * @group Database |
| 6 | + * @group Destructive |
| 7 | + */ |
| 8 | + |
| 9 | +/** |
| 10 | + * n.b. Ensure that you can write to the images/ directory as the |
| 11 | + * user that will run tests. |
| 12 | + */ |
| 13 | + |
| 14 | +// Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on. |
| 15 | +// This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...) |
| 16 | +// (and in the case of the other Upload tests, this flat out just actually works... ) |
| 17 | + |
| 18 | +// TODO: refactor into several files |
| 19 | +// TODO: port the other Upload tests, and other API tests to this framework |
| 20 | + |
| 21 | +require_once( dirname( __FILE__ ) . '/RandomImageGenerator.php' ); |
| 22 | + |
| 23 | +/* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */ |
| 24 | +class ApiTestUser { |
| 25 | + public $username; |
| 26 | + public $password; |
| 27 | + public $email; |
| 28 | + public $groups; |
| 29 | + public $user; |
| 30 | + |
| 31 | + function __construct( $username, $realname = 'Real Name', $email = 'sample@sample.com', $groups = array() ) { |
| 32 | + global $wgMinimalPasswordLength; |
| 33 | + |
| 34 | + $this->username = $username; |
| 35 | + $this->realname = $realname; |
| 36 | + $this->email = $email; |
| 37 | + $this->groups = $groups; |
| 38 | + |
| 39 | + // don't allow user to hardcode or select passwords -- people sometimes run tests |
| 40 | + // on live wikis. Sometimes we create sysop users in these tests. A sysop user with |
| 41 | + // a known password would be a Bad Thing. |
| 42 | + $this->password = User::randomPassword(); |
| 43 | + |
| 44 | + $this->user = User::newFromName( $this->username ); |
| 45 | + $this->user->load(); |
| 46 | + |
| 47 | + // In an ideal world we'd have a new wiki (or mock data store) for every single test. |
| 48 | + // But for now, we just need to create or update the user with the desired properties. |
| 49 | + // we particularly need the new password, since we just generated it randomly. |
| 50 | + // In core MediaWiki, there is no functionality to delete users, so this is the best we can do. |
| 51 | + if ( !$this->user->getID() ) { |
| 52 | + // create the user |
| 53 | + $this->user = User::createNew( |
| 54 | + $this->username, array( |
| 55 | + "email" => $this->email, |
| 56 | + "real_name" => $this->realname |
| 57 | + ) |
| 58 | + ); |
| 59 | + if ( !$this->user ) { |
| 60 | + throw new Exception( "error creating user" ); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // update the user to use the new random password and other details |
| 65 | + $this->user->setPassword( $this->password ); |
| 66 | + $this->user->setEmail( $this->email ); |
| 67 | + $this->user->setRealName( $this->realname ); |
| 68 | + // remove all groups, replace with any groups specified |
| 69 | + foreach ( $this->user->getGroups() as $group ) { |
| 70 | + $this->user->removeGroup( $group ); |
| 71 | + } |
| 72 | + if ( count( $this->groups ) ) { |
| 73 | + foreach ( $this->groups as $group ) { |
| 74 | + $this->user->addGroup( $group ); |
| 75 | + } |
| 76 | + } |
| 77 | + $this->user->saveSettings(); |
| 78 | + |
| 79 | + } |
| 80 | + |
| 81 | +} |
| 82 | + |
| 83 | +abstract class ApiTestCase extends PHPUnit_Framework_TestCase { |
| 84 | + public static $users; |
| 85 | + |
| 86 | + function setUp() { |
| 87 | + global $wgServer, $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser; |
| 88 | + |
| 89 | + $wgMemc = new FakeMemCachedClient(); |
| 90 | + $wgContLang = Language::factory( 'en' ); |
| 91 | + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); |
| 92 | + $wgRequest = new FauxRequest( array() ); |
| 93 | + |
| 94 | + self::$users = array( |
| 95 | + 'sysop' => new ApiTestUser( |
| 96 | + 'Apitestsysop', |
| 97 | + 'Api Test Sysop', |
| 98 | + 'api_test_sysop@sample.com', |
| 99 | + array( 'sysop' ) |
| 100 | + ), |
| 101 | + 'uploader' => new ApiTestUser( |
| 102 | + 'Apitestuser', |
| 103 | + 'Api Test User', |
| 104 | + 'api_test_user@sample.com', |
| 105 | + array() |
| 106 | + ) |
| 107 | + ); |
| 108 | + |
| 109 | + $wgUser = self::$users['sysop']->user; |
| 110 | + |
| 111 | + } |
| 112 | + |
| 113 | + function tearDown() { |
| 114 | + global $wgMemc; |
| 115 | + $wgMemc = null; |
| 116 | + } |
| 117 | + |
| 118 | + protected function doApiRequest( $params, $session = null ) { |
| 119 | + $_SESSION = isset( $session ) ? $session : array(); |
| 120 | + |
| 121 | + $request = new FauxRequest( $params, true, $_SESSION ); |
| 122 | + $module = new ApiMain( $request, true ); |
| 123 | + $module->execute(); |
| 124 | + |
| 125 | + return array( $module->getResultData(), $request, $_SESSION ); |
| 126 | + } |
| 127 | + |
| 128 | + /** |
| 129 | + * Add an edit token to the API request |
| 130 | + * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the |
| 131 | + * request, without actually requesting a "real" edit token |
| 132 | + * @param $params: key-value API params |
| 133 | + * @param $data: a structure which also contains the session |
| 134 | + */ |
| 135 | + protected function doApiRequestWithToken( $params, $session ) { |
| 136 | + if ( $session['wsToken'] ) { |
| 137 | + // add edit token to fake session |
| 138 | + $session['wsEditToken'] = $session['wsToken']; |
| 139 | + // add token to request parameters |
| 140 | + $params['token'] = md5( $session['wsToken'] ) . EDIT_TOKEN_SUFFIX; |
| 141 | + return $this->doApiRequest( $params, $session ); |
| 142 | + } else { |
| 143 | + throw new Exception( "request data not in right format" ); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | +} |
| 148 | + |
| 149 | +class ApiUploadTest extends ApiTestCase { |
| 150 | + /** |
| 151 | + * Fixture -- run before every test |
| 152 | + */ |
| 153 | + public function setUp() { |
| 154 | + global $wgEnableUploads, $wgEnableAPI, $wgDebugLogFile; |
| 155 | + parent::setUp(); |
| 156 | + |
| 157 | + $wgEnableUploads = true; |
| 158 | + $wgEnableAPI = true; |
| 159 | + wfSetupSession(); |
| 160 | + |
| 161 | + $wgDebugLogFile = '/private/tmp/mwtestdebug.log'; |
| 162 | + ini_set( 'log_errors', 1 ); |
| 163 | + ini_set( 'error_reporting', 1 ); |
| 164 | + ini_set( 'display_errors', 1 ); |
| 165 | + |
| 166 | + $this->clearFakeUploads(); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * Fixture -- run after every test |
| 171 | + * Clean up temporary files etc. |
| 172 | + */ |
| 173 | + function tearDown() { |
| 174 | + } |
| 175 | + |
| 176 | + |
| 177 | + /** |
| 178 | + * Testing login |
| 179 | + * XXX this is a funny way of getting session context |
| 180 | + */ |
| 181 | + function testLogin() { |
| 182 | + $user = self::$users['uploader']; |
| 183 | + |
| 184 | + $params = array( |
| 185 | + 'action' => 'login', |
| 186 | + 'lgname' => $user->username, |
| 187 | + 'lgpassword' => $user->password |
| 188 | + ); |
| 189 | + list( $result, $request, $session ) = $this->doApiRequest( $params ); |
| 190 | + $this->assertArrayHasKey( "login", $result ); |
| 191 | + $this->assertArrayHasKey( "result", $result['login'] ); |
| 192 | + $this->assertEquals( "NeedToken", $result['login']['result'] ); |
| 193 | + $token = $result['login']['token']; |
| 194 | + |
| 195 | + $params = array( |
| 196 | + 'action' => 'login', |
| 197 | + 'lgtoken' => $token, |
| 198 | + 'lgname' => $user->username, |
| 199 | + 'lgpassword' => $user->password |
| 200 | + ); |
| 201 | + list( $result, $request, $session ) = $this->doApiRequest( $params ); |
| 202 | + $this->assertArrayHasKey( "login", $result ); |
| 203 | + $this->assertArrayHasKey( "result", $result['login'] ); |
| 204 | + $this->assertEquals( "Success", $result['login']['result'] ); |
| 205 | + $this->assertArrayHasKey( 'lgtoken', $result['login'] ); |
| 206 | + |
| 207 | + return $session; |
| 208 | + |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * @depends testLogin |
| 213 | + */ |
| 214 | + public function testUploadRequiresToken( $session ) { |
| 215 | + $exception = false; |
| 216 | + try { |
| 217 | + $this->doApiRequest( array( |
| 218 | + 'action' => 'upload' |
| 219 | + ) ); |
| 220 | + } catch ( UsageException $e ) { |
| 221 | + $exception = true; |
| 222 | + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); |
| 223 | + } |
| 224 | + $this->assertTrue( $exception, "Got exception" ); |
| 225 | + } |
| 226 | + |
| 227 | + /** |
| 228 | + * @depends testLogin |
| 229 | + */ |
| 230 | + public function testUploadMissingParams( $session ) { |
| 231 | + global $wgUser; |
| 232 | + $wgUser = self::$users['uploader']->user; |
| 233 | + |
| 234 | + $exception = false; |
| 235 | + try { |
| 236 | + $this->doApiRequestWithToken( array( |
| 237 | + 'action' => 'upload', |
| 238 | + ), $session ); |
| 239 | + } catch ( UsageException $e ) { |
| 240 | + $exception = true; |
| 241 | + $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", |
| 242 | + $e->getMessage() ); |
| 243 | + } |
| 244 | + $this->assertTrue( $exception, "Got exception" ); |
| 245 | + } |
| 246 | + |
| 247 | + |
| 248 | + /** |
| 249 | + * @depends testLogin |
| 250 | + */ |
| 251 | + public function testUpload( $session ) { |
| 252 | + global $wgUser; |
| 253 | + $wgUser = self::$users['uploader']->user; |
| 254 | + |
| 255 | + $extension = 'png'; |
| 256 | + $mimeType = 'image/png'; |
| 257 | + |
| 258 | + $randomImageGenerator = new RandomImageGenerator(); |
| 259 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 260 | + $filePath = $filePaths[0]; |
| 261 | + $fileName = basename( $filePath ); |
| 262 | + |
| 263 | + $this->deleteFileByFileName( $fileName ); |
| 264 | + $this->deleteFileByContent( $filePath ); |
| 265 | + |
| 266 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 267 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 268 | + } |
| 269 | + |
| 270 | + $params = array( |
| 271 | + 'action' => 'upload', |
| 272 | + 'filename' => $fileName, |
| 273 | + 'file' => 'dummy content', |
| 274 | + 'comment' => 'dummy comment', |
| 275 | + 'text' => "This is the page text for $fileName", |
| 276 | + ); |
| 277 | + |
| 278 | + $exception = false; |
| 279 | + try { |
| 280 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 281 | + } catch ( UsageException $e ) { |
| 282 | + $exception = true; |
| 283 | + } |
| 284 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 285 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 286 | + $this->assertFalse( $exception ); |
| 287 | + |
| 288 | + // clean up |
| 289 | + $this->deleteFileByFilename( $fileName ); |
| 290 | + unlink( $filePath ); |
| 291 | + } |
| 292 | + |
| 293 | + |
| 294 | + /** |
| 295 | + * @depends testLogin |
| 296 | + */ |
| 297 | + public function testUploadZeroLength( $session ) { |
| 298 | + global $wgUser; |
| 299 | + $wgUser = self::$users['uploader']->user; |
| 300 | + |
| 301 | + $extension = 'png'; |
| 302 | + $mimeType = 'image/png'; |
| 303 | + |
| 304 | + $filePath = tempnam( wfTempDir(), "" ); |
| 305 | + $fileName = "apiTestUploadZeroLength.png"; |
| 306 | + |
| 307 | + $this->deleteFileByFileName( $fileName ); |
| 308 | + |
| 309 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 310 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 311 | + } |
| 312 | + |
| 313 | + $params = array( |
| 314 | + 'action' => 'upload', |
| 315 | + 'filename' => $fileName, |
| 316 | + 'file' => 'dummy content', |
| 317 | + 'comment' => 'dummy comment', |
| 318 | + 'text' => "This is the page text for $fileName", |
| 319 | + ); |
| 320 | + |
| 321 | + $exception = false; |
| 322 | + try { |
| 323 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 324 | + } catch ( UsageException $e ) { |
| 325 | + $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); |
| 326 | + $exception = true; |
| 327 | + } |
| 328 | + $this->assertTrue( $exception ); |
| 329 | + |
| 330 | + // clean up |
| 331 | + $this->deleteFileByFilename( $fileName ); |
| 332 | + unlink( $filePath ); |
| 333 | + } |
| 334 | + |
| 335 | + |
| 336 | + /** |
| 337 | + * @depends testLogin |
| 338 | + */ |
| 339 | + public function testUploadSameFileName( $session ) { |
| 340 | + global $wgUser; |
| 341 | + $wgUser = self::$users['uploader']->user; |
| 342 | + |
| 343 | + $extension = 'png'; |
| 344 | + $mimeType = 'image/png'; |
| 345 | + |
| 346 | + $randomImageGenerator = new RandomImageGenerator(); |
| 347 | + $filePaths = $randomImageGenerator->writeImages( 2, $extension, dirname( wfTempDir() ) ); |
| 348 | + // we'll reuse this filename |
| 349 | + $fileName = basename( $filePaths[0] ); |
| 350 | + |
| 351 | + // clear any other files with the same name |
| 352 | + $this->deleteFileByFileName( $fileName ); |
| 353 | + |
| 354 | + // we reuse these params |
| 355 | + $params = array( |
| 356 | + 'action' => 'upload', |
| 357 | + 'filename' => $fileName, |
| 358 | + 'file' => 'dummy content', |
| 359 | + 'comment' => 'dummy comment', |
| 360 | + 'text' => "This is the page text for $fileName", |
| 361 | + ); |
| 362 | + |
| 363 | + // first upload .... should succeed |
| 364 | + |
| 365 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) { |
| 366 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 367 | + } |
| 368 | + |
| 369 | + $exception = false; |
| 370 | + try { |
| 371 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 372 | + } catch ( UsageException $e ) { |
| 373 | + $exception = true; |
| 374 | + } |
| 375 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 376 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 377 | + $this->assertFalse( $exception ); |
| 378 | + |
| 379 | + // second upload with the same name (but different content) |
| 380 | + |
| 381 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) { |
| 382 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 383 | + } |
| 384 | + |
| 385 | + $exception = false; |
| 386 | + try { |
| 387 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 388 | + } catch ( UsageException $e ) { |
| 389 | + $exception = true; |
| 390 | + } |
| 391 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 392 | + $this->assertEquals( 'Warning', $result['upload']['result'] ); |
| 393 | + $this->assertTrue( isset( $result['upload']['warnings'] ) ); |
| 394 | + $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) ); |
| 395 | + $this->assertFalse( $exception ); |
| 396 | + |
| 397 | + // clean up |
| 398 | + $this->deleteFileByFilename( $fileName ); |
| 399 | + unlink( $filePaths[0] ); |
| 400 | + unlink( $filePaths[1] ); |
| 401 | + } |
| 402 | + |
| 403 | + |
| 404 | + /** |
| 405 | + * @depends testLogin |
| 406 | + */ |
| 407 | + public function testUploadSameContent( $session ) { |
| 408 | + global $wgUser; |
| 409 | + $wgUser = self::$users['uploader']->user; |
| 410 | + |
| 411 | + $extension = 'png'; |
| 412 | + $mimeType = 'image/png'; |
| 413 | + |
| 414 | + $randomImageGenerator = new RandomImageGenerator(); |
| 415 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 416 | + $fileNames[0] = basename( $filePaths[0] ); |
| 417 | + $fileNames[1] = "SameContentAs" . $fileNames[0]; |
| 418 | + |
| 419 | + // clear any other files with the same name or content |
| 420 | + $this->deleteFileByContent( $filePaths[0] ); |
| 421 | + $this->deleteFileByFileName( $fileNames[0] ); |
| 422 | + $this->deleteFileByFileName( $fileNames[1] ); |
| 423 | + |
| 424 | + // first upload .... should succeed |
| 425 | + |
| 426 | + $params = array( |
| 427 | + 'action' => 'upload', |
| 428 | + 'filename' => $fileNames[0], |
| 429 | + 'file' => 'dummy content', |
| 430 | + 'comment' => 'dummy comment', |
| 431 | + 'text' => "This is the page text for " . $fileNames[0], |
| 432 | + ); |
| 433 | + |
| 434 | + if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) { |
| 435 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 436 | + } |
| 437 | + |
| 438 | + $exception = false; |
| 439 | + try { |
| 440 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 441 | + } catch ( UsageException $e ) { |
| 442 | + $exception = true; |
| 443 | + } |
| 444 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 445 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 446 | + $this->assertFalse( $exception ); |
| 447 | + |
| 448 | + |
| 449 | + // second upload with the same content (but different name) |
| 450 | + |
| 451 | + if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) { |
| 452 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 453 | + } |
| 454 | + |
| 455 | + $params = array( |
| 456 | + 'action' => 'upload', |
| 457 | + 'filename' => $fileNames[1], |
| 458 | + 'file' => 'dummy content', |
| 459 | + 'comment' => 'dummy comment', |
| 460 | + 'text' => "This is the page text for " . $fileNames[1], |
| 461 | + ); |
| 462 | + |
| 463 | + $exception = false; |
| 464 | + try { |
| 465 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 466 | + } catch ( UsageException $e ) { |
| 467 | + $exception = true; |
| 468 | + } |
| 469 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 470 | + $this->assertEquals( 'Warning', $result['upload']['result'] ); |
| 471 | + $this->assertTrue( isset( $result['upload']['warnings'] ) ); |
| 472 | + $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) ); |
| 473 | + $this->assertFalse( $exception ); |
| 474 | + |
| 475 | + // clean up |
| 476 | + $this->deleteFileByFilename( $fileNames[0] ); |
| 477 | + $this->deleteFileByFilename( $fileNames[1] ); |
| 478 | + unlink( $filePaths[0] ); |
| 479 | + } |
| 480 | + |
| 481 | + |
| 482 | + /** |
| 483 | + * @depends testLogin |
| 484 | + */ |
| 485 | + public function testUploadStash( $session ) { |
| 486 | + global $wgUser; |
| 487 | + $wgUser = self::$users['uploader']->user; |
| 488 | + |
| 489 | + $extension = 'png'; |
| 490 | + $mimeType = 'image/png'; |
| 491 | + |
| 492 | + $randomImageGenerator = new RandomImageGenerator(); |
| 493 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 494 | + $filePath = $filePaths[0]; |
| 495 | + $fileName = basename( $filePath ); |
| 496 | + |
| 497 | + $this->deleteFileByFileName( $fileName ); |
| 498 | + $this->deleteFileByContent( $filePath ); |
| 499 | + |
| 500 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 501 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 502 | + } |
| 503 | + |
| 504 | + $params = array( |
| 505 | + 'action' => 'upload', |
| 506 | + 'stash' => 1, |
| 507 | + 'filename' => $fileName, |
| 508 | + 'file' => 'dummy content', |
| 509 | + 'comment' => 'dummy comment', |
| 510 | + 'text' => "This is the page text for $fileName", |
| 511 | + ); |
| 512 | + |
| 513 | + $exception = false; |
| 514 | + try { |
| 515 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 516 | + } catch ( UsageException $e ) { |
| 517 | + $exception = true; |
| 518 | + } |
| 519 | + $this->assertFalse( $exception ); |
| 520 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 521 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 522 | + $this->assertTrue( isset( $result['upload']['sessionkey'] ) ); |
| 523 | + $sessionkey = $result['upload']['sessionkey']; |
| 524 | + |
| 525 | + // it should be visible from Special:UploadStash |
| 526 | + // XXX ...but how to test this, with a fake WebRequest with the session? |
| 527 | + |
| 528 | + // now we should try to release the file from stash |
| 529 | + $params = array( |
| 530 | + 'action' => 'upload', |
| 531 | + 'sessionkey' => $sessionkey, |
| 532 | + 'filename' => $fileName, |
| 533 | + 'comment' => 'dummy comment', |
| 534 | + 'text' => "This is the page text for $fileName, altered", |
| 535 | + ); |
| 536 | + |
| 537 | + $this->clearFakeUploads(); |
| 538 | + $exception = false; |
| 539 | + try { |
| 540 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 541 | + } catch ( UsageException $e ) { |
| 542 | + $exception = true; |
| 543 | + } |
| 544 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 545 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 546 | + $this->assertFalse( $exception ); |
| 547 | + |
| 548 | + // clean up |
| 549 | + $this->deleteFileByFilename( $fileName ); |
| 550 | + unlink( $filePath ); |
| 551 | + } |
| 552 | + |
| 553 | + |
| 554 | + |
| 555 | + /** |
| 556 | + * Helper function -- remove files and associated articles by Title |
| 557 | + * @param {Title} title to be removed |
| 558 | + */ |
| 559 | + public function deleteFileByTitle( $title ) { |
| 560 | + if ( $title->exists() ) { |
| 561 | + $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) ); |
| 562 | + $noOldArchive = ""; // yes this really needs to be set this way |
| 563 | + $comment = "removing for test"; |
| 564 | + $restrictDeletedVersions = false; |
| 565 | + $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions ); |
| 566 | + if ( !$status->isGood() ) { |
| 567 | + return false; |
| 568 | + } |
| 569 | + $article = new Article( $title ); |
| 570 | + $article->doDeleteArticle( "removing for test" ); |
| 571 | + |
| 572 | + // see if it now doesn't exist; reload |
| 573 | + $title = Title::newFromText( $fileName, NS_FILE ); |
| 574 | + } |
| 575 | + return ! ( $title && is_a( $title, 'Title' ) && $title->exists() ); |
| 576 | + } |
| 577 | + |
| 578 | + /** |
| 579 | + * Helper function -- remove files and associated articles with a particular filename |
| 580 | + * @param {String} filename to be removed |
| 581 | + */ |
| 582 | + public function deleteFileByFileName( $fileName ) { |
| 583 | + return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) ); |
| 584 | + } |
| 585 | + |
| 586 | + |
| 587 | + /** |
| 588 | + * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them. |
| 589 | + * @param {String} path to file on the filesystem |
| 590 | + */ |
| 591 | + public function deleteFileByContent( $filePath ) { |
| 592 | + $hash = File::sha1Base36( $filePath ); |
| 593 | + $dupes = RepoGroup::singleton()->findBySha1( $hash ); |
| 594 | + $success = true; |
| 595 | + foreach ( $dupes as $key => $dupe ) { |
| 596 | + $success &= $this->deleteFileByTitle( $dupe->getTitle() ); |
| 597 | + } |
| 598 | + return $success; |
| 599 | + } |
| 600 | + |
| 601 | + /** |
| 602 | + * Fake an upload by dumping the file into temp space, and adding info to $_FILES. |
| 603 | + * (This is what PHP would normally do). |
| 604 | + * @param {String}: fieldname - name this would have in the upload form |
| 605 | + * @param {String}: fileName - name to title this |
| 606 | + * @param {String}: mime type |
| 607 | + * @param {String}: filePath - path where to find file contents |
| 608 | + */ |
| 609 | + function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { |
| 610 | + $tmpName = tempnam( wfTempDir(), "" ); |
| 611 | + if ( !file_exists( $filePath ) ) { |
| 612 | + throw new Exception( "$filePath doesn't exist!" ); |
| 613 | + }; |
| 614 | + |
| 615 | + if ( !copy( $filePath, $tmpName ) ) { |
| 616 | + throw new Exception( "couldn't copy $filePath to $tmpName" ); |
| 617 | + } |
| 618 | + |
| 619 | + clearstatcache(); |
| 620 | + $size = filesize( $tmpName ); |
| 621 | + if ( $size === false ) { |
| 622 | + throw new Exception( "couldn't stat $tmpName" ); |
| 623 | + } |
| 624 | + |
| 625 | + $_FILES[ $fieldName ] = array( |
| 626 | + 'name' => $fileName, |
| 627 | + 'type' => $type, |
| 628 | + 'tmp_name' => $tmpName, |
| 629 | + 'size' => $size, |
| 630 | + 'error' => null |
| 631 | + ); |
| 632 | + |
| 633 | + return true; |
| 634 | + |
| 635 | + } |
| 636 | + |
| 637 | + /** |
| 638 | + * Remove traces of previous fake uploads |
| 639 | + */ |
| 640 | + function clearFakeUploads() { |
| 641 | + $_FILES = array(); |
| 642 | + } |
| 643 | + |
| 644 | + |
| 645 | +} |
| 646 | + |
Property changes on: trunk/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1 | 647 | Merged /trunk/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php:r73549-75058,75060-75821 |
2 | 648 | Merged /branches/phpunit-restructure/maintenance/tests/phpunit/includes/api/ApiUploadTest.php:r72257-72560 |
Added: svn:eol-style |
3 | 649 | + native |
Index: trunk/phase3/maintenance/tests/phpunit/includes/api/generateRandomImages.php |
— | — | @@ -0,0 +1,25 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +require("RandomImageGenerator.php"); |
| 5 | + |
| 6 | +$getOptSpec = array( |
| 7 | + 'dictionaryFile::', |
| 8 | + 'minWidth::', |
| 9 | + 'maxWidth::', |
| 10 | + 'minHeight::', |
| 11 | + 'maxHeight::', |
| 12 | + 'circlesToDraw::', |
| 13 | + |
| 14 | + 'number::', |
| 15 | + 'format::' |
| 16 | +); |
| 17 | +$options = getopt( null, $getOptSpec ); |
| 18 | + |
| 19 | +$format = isset( $options['format'] ) ? $options['format'] : 'jpg'; |
| 20 | +unset( $options['format'] ); |
| 21 | + |
| 22 | +$number = isset( $options['number'] ) ? int( $options['number'] ) : 10; |
| 23 | +unset( $options['number'] ); |
| 24 | + |
| 25 | +$randomImageGenerator = new RandomImageGenerator( $options ); |
| 26 | +$randomImageGenerator->writeImages( $number, $format ); |
Property changes on: trunk/phase3/maintenance/tests/phpunit/includes/api/generateRandomImages.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1 | 27 | Merged /branches/phpunit-restructure/maintenance/tests/phpunit/includes/api/generateRandomImages.php:r72257-72560 |
2 | 28 | Merged /trunk/phase3/maintenance/tests/phpunit/includes/api/generateRandomImages.php:r73549-75058,75060-75821 |
Added: svn:eol-style |
3 | 29 | + native |
Index: trunk/phase3/maintenance/tests/phpunit/Makefile |
— | — | @@ -4,7 +4,8 @@ |
5 | 5 | SHELL = /bin/sh |
6 | 6 | CONFIG_FILE = $(shell pwd)/suite.xml |
7 | 7 | FLAGS = |
8 | | -PU = php phpunit.php --configuration ${CONFIG_FILE} |
| 8 | +PHP = php |
| 9 | +PU = ${PHP} phpunit.php --configuration ${CONFIG_FILE} |
9 | 10 | |
10 | 11 | all test: warning |
11 | 12 | |
— | — | @@ -73,3 +74,4 @@ |
74 | 75 | # Options: |
75 | 76 | # CONFIG_FILE Path to a PHPUnit configuration file (default: suite.xml) |
76 | 77 | # FLAGS Additional flags to pass to PHPUnit |
| 78 | + # PHP Path to php |
Property changes on: trunk/phase3/maintenance/tests/phpunit/Makefile |
___________________________________________________________________ |
Added: svn:mergeinfo |
77 | 79 | Merged /branches/uploadwizard/phase3/maintenance/tests/phpunit/Makefile:r73550-75905 |
78 | 80 | Merged /branches/phpunit-restructure/maintenance/tests/phpunit/Makefile:r72257-72560 |
Index: trunk/phase3/includes/upload/UploadBase.php |
— | — | @@ -600,6 +600,9 @@ |
601 | 601 | } |
602 | 602 | |
603 | 603 | /** |
| 604 | + * NOTE: Probably should be deprecated in favor of UploadStash, but this is sometimes |
| 605 | + * called outside that context. |
| 606 | + * |
604 | 607 | * Stash a file in a temporary directory for later processing |
605 | 608 | * after the user has confirmed it. |
606 | 609 | * |
— | — | @@ -617,40 +620,36 @@ |
618 | 621 | } |
619 | 622 | |
620 | 623 | /** |
621 | | - * Stash a file in a temporary directory for later processing, |
622 | | - * and save the necessary descriptive info into the session. |
623 | | - * Returns a key value which will be passed through a form |
624 | | - * to pick up the path info on a later invocation. |
| 624 | + * If the user does not supply all necessary information in the first upload form submission (either by accident or |
| 625 | + * by design) then we may want to stash the file temporarily, get more information, and publish the file later. |
625 | 626 | * |
626 | | - * @return Integer: session key |
| 627 | + * This method will stash a file in a temporary directory for later processing, and save the necessary descriptive info |
| 628 | + * into the user's session. |
| 629 | + * This method returns the file object, which also has a 'sessionKey' property which can be passed through a form or |
| 630 | + * API request to find this stashed file again. |
| 631 | + * |
| 632 | + * @param {String}: $key (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated. |
| 633 | + * @return {File}: stashed file |
627 | 634 | */ |
628 | | - public function stashSession( $key = null ) { |
629 | | - $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); |
630 | | - if( !$status->isOK() ) { |
631 | | - # Couldn't save the file. |
632 | | - return false; |
633 | | - } |
634 | | - |
635 | | - if ( is_null( $key ) ) { |
636 | | - $key = $this->getSessionKey(); |
637 | | - } |
638 | | - $_SESSION[self::SESSION_KEYNAME][$key] = array( |
639 | | - 'mTempPath' => $status->value, |
640 | | - 'mFileSize' => $this->mFileSize, |
641 | | - 'mFileProps' => $this->mFileProps, |
642 | | - 'version' => self::SESSION_VERSION, |
| 635 | + public function stashSessionFile( $key = null ) { |
| 636 | + $stash = new UploadStash(); |
| 637 | + $data = array( |
| 638 | + 'mFileProps' => $this->mFileProps |
643 | 639 | ); |
644 | | - return $key; |
| 640 | + $file = $stash->stashFile( $this->mTempPath, $data, $key ); |
| 641 | + // TODO should we change the "local file" here? |
| 642 | + // $this->mLocalFile = $file; |
| 643 | + return $file; |
645 | 644 | } |
646 | 645 | |
647 | 646 | /** |
648 | | - * Generate a random session key from stash in cases where we want |
649 | | - * to start an upload without much information |
| 647 | + * Stash a file in a temporary directory, returning a key which can be used to find the file again. See stashSessionFile(). |
| 648 | + * |
| 649 | + * @param {String}: $key (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated. |
| 650 | + * @return {String}: session key |
650 | 651 | */ |
651 | | - protected function getSessionKey() { |
652 | | - $key = mt_rand( 0, 0x7fffffff ); |
653 | | - $_SESSION[self::SESSION_KEYNAME][$key] = array(); |
654 | | - return $key; |
| 652 | + public function stashSession( $key = null ) { |
| 653 | + return $this->stashSessionFile( $key )->getSessionKey(); |
655 | 654 | } |
656 | 655 | |
657 | 656 | /** |
— | — | @@ -1197,12 +1196,23 @@ |
1198 | 1197 | return $blacklist; |
1199 | 1198 | } |
1200 | 1199 | |
| 1200 | + /** |
| 1201 | + * Gets image info about the file just uploaded. |
| 1202 | + * |
| 1203 | + * Also has the effect of setting metadata to be an 'indexed tag name' in returned API result if |
| 1204 | + * 'metadata' was requested. Oddly, we have to pass the "result" object down just so it can do that |
| 1205 | + * with the appropriate format, presumably. |
| 1206 | + * |
| 1207 | + * @param {ApiResult} |
| 1208 | + * @return {Array} image info |
| 1209 | + */ |
1201 | 1210 | public function getImageInfo( $result ) { |
1202 | 1211 | $file = $this->getLocalFile(); |
1203 | 1212 | $imParam = ApiQueryImageInfo::getPropertyNames(); |
1204 | 1213 | return ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result ); |
1205 | 1214 | } |
1206 | 1215 | |
| 1216 | + |
1207 | 1217 | public function convertVerifyErrorToStatus( $error ) { |
1208 | 1218 | $code = $error['status']; |
1209 | 1219 | unset( $code['status'] ); |
Property changes on: trunk/phase3/includes/upload/UploadBase.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1210 | 1220 | Merged /branches/sqlite/includes/upload/UploadBase.php:r58211-58321 |
1211 | 1221 | Merged /branches/new-installer/phase3/includes/upload/UploadBase.php:r43664-66004 |
1212 | 1222 | Merged /branches/wmf-deployment/includes/upload/UploadBase.php:r53381 |
1213 | 1223 | Merged /branches/uploadwizard/phase3/includes/upload/UploadBase.php:r73550-75905 |
1214 | 1224 | Merged /branches/REL1_15/phase3/includes/upload/UploadBase.php:r51646 |
Index: trunk/phase3/includes/upload/UploadStash.php |
— | — | @@ -0,0 +1,406 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * UploadStash is intended to accomplish a few things: |
| 5 | + * - enable applications to temporarily stash files without publishing them to the wiki. |
| 6 | + * - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension |
| 7 | + * And there are several that reimplement stashing from scratch, in idiosyncratic ways. The idea is to unify them all here. |
| 8 | + * Mostly all of them are the same except for storing some custom fields, which we subsume into the data array. |
| 9 | + * - enable applications to find said files later, as long as the session or temp files haven't been purged. |
| 10 | + * - enable the uploading user (and *ONLY* the uploading user) to access said files, and thumbnails of said files, via a URL. |
| 11 | + * We accomplish this by making the session serve as a URL->file mapping, on the assumption that nobody else can access |
| 12 | + * the session, even the uploading user. See SpecialUploadStash, which implements a web interface to some files stored this way. |
| 13 | + * |
| 14 | + */ |
| 15 | +class UploadStash { |
| 16 | + // Format of the key for files -- has to be suitable as a filename itself in some cases. |
| 17 | + // This should encompass a sha1 content hash in hex (new style), or an integer (old style), |
| 18 | + // and also thumbnails with prepended strings like "120px-". |
| 19 | + // The file extension should not be part of the key. |
| 20 | + const KEY_FORMAT_REGEX = '/^[\w-]+$/'; |
| 21 | + |
| 22 | + // repository that this uses to store temp files |
| 23 | + protected $repo; |
| 24 | + |
| 25 | + // array of initialized objects obtained from session (lazily initialized upon getFile()) |
| 26 | + private $files = array(); |
| 27 | + |
| 28 | + // the base URL for files in the stash |
| 29 | + private $baseUrl; |
| 30 | + |
| 31 | + // TODO: Once UploadBase starts using this, switch to use these constants rather than UploadBase::SESSION* |
| 32 | + // const SESSION_VERSION = 2; |
| 33 | + // const SESSION_KEYNAME = 'wsUploadData'; |
| 34 | + |
| 35 | + /** |
| 36 | + * Represents the session which contains temporarily stored files. |
| 37 | + * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually) |
| 38 | + * @param {FileRepo} $repo: optional -- repo in which to store files. Will choose LocalRepo if not supplied. |
| 39 | + */ |
| 40 | + public function __construct( $repo = null ) { |
| 41 | + |
| 42 | + if ( is_null( $repo ) ) { |
| 43 | + $repo = RepoGroup::singleton()->getLocalRepo(); |
| 44 | + } |
| 45 | + |
| 46 | + $this->repo = $repo; |
| 47 | + |
| 48 | + if ( ! isset( $_SESSION ) ) { |
| 49 | + throw new UploadStashNotAvailableException( 'no session variable' ); |
| 50 | + } |
| 51 | + |
| 52 | + if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME] ) ) { |
| 53 | + $_SESSION[UploadBase::SESSION_KEYNAME] = array(); |
| 54 | + } |
| 55 | + |
| 56 | + $this->baseUrl = SpecialPage::getTitleFor( 'UploadStash' )->getLocalURL(); |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * Get the base of URLs by which one can access the files |
| 61 | + * @return {String} url |
| 62 | + */ |
| 63 | + public function getBaseUrl() { |
| 64 | + return $this->baseUrl; |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Get a file and its metadata from the stash. |
| 69 | + * May throw exception if session data cannot be parsed due to schema change, or key not found. |
| 70 | + * @param {Integer} $key: key |
| 71 | + * @throws UploadStashFileNotFoundException |
| 72 | + * @throws UploadStashBadVersionException |
| 73 | + * @return {UploadStashItem} null if no such item or item out of date, or the item |
| 74 | + */ |
| 75 | + public function getFile( $key ) { |
| 76 | + if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
| 77 | + throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); |
| 78 | + } |
| 79 | + |
| 80 | + if ( !isset( $this->files[$key] ) ) { |
| 81 | + if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME][$key] ) ) { |
| 82 | + throw new UploadStashFileNotFoundException( "key '$key' not found in session" ); |
| 83 | + } |
| 84 | + |
| 85 | + $data = $_SESSION[UploadBase::SESSION_KEYNAME][$key]; |
| 86 | + // guards against PHP class changing while session data doesn't |
| 87 | + if ($data['version'] !== UploadBase::SESSION_VERSION ) { |
| 88 | + throw new UploadStashBadVersionException( $data['version'] . " does not match current version " . UploadBase::SESSION_VERSION ); |
| 89 | + } |
| 90 | + |
| 91 | + // separate the stashData into the path, and then the rest of the data |
| 92 | + $path = $data['mTempPath']; |
| 93 | + unset( $data['mTempPath'] ); |
| 94 | + |
| 95 | + $file = new UploadStashFile( $this, $this->repo, $path, $key, $data ); |
| 96 | + |
| 97 | + $this->files[$key] = $file; |
| 98 | + |
| 99 | + } |
| 100 | + return $this->files[$key]; |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Stash a file in a temp directory and record that we did this in the session, along with other metadata. |
| 105 | + * We store data in a flat key-val namespace because that's how UploadBase did it. This also means we have to |
| 106 | + * ensure that the key-val pairs in $data do not overwrite other required fields. |
| 107 | + * |
| 108 | + * @param {String} $path: path to file you want stashed |
| 109 | + * @param {Array} $data: optional, other data you want associated with the file. Do not use 'mTempPath', 'mFileProps', 'mFileSize', or 'version' as keys here |
| 110 | + * @param {String} $key: optional, unique key for this file in this session. Used for directory hashing when storing, otherwise not important |
| 111 | + * @throws UploadStashBadPathException |
| 112 | + * @throws UploadStashFileException |
| 113 | + * @return {null|UploadStashFile} file, or null on failure |
| 114 | + */ |
| 115 | + public function stashFile( $path, $data = array(), $key = null ) { |
| 116 | + if ( ! file_exists( $path ) ) { |
| 117 | + throw new UploadStashBadPathException( "path '$path' doesn't exist" ); |
| 118 | + } |
| 119 | + $fileProps = File::getPropsFromPath( $path ); |
| 120 | + |
| 121 | + // If no key was supplied, use content hash. Also has the nice property of collapsing multiple identical files |
| 122 | + // uploaded this session, which could happen if uploads had failed. |
| 123 | + if ( is_null( $key ) ) { |
| 124 | + $key = $fileProps['sha1']; |
| 125 | + } |
| 126 | + |
| 127 | + if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) { |
| 128 | + throw new UploadStashBadPathException( "key '$key' is not in a proper format" ); |
| 129 | + } |
| 130 | + |
| 131 | + // if not already in a temporary area, put it there |
| 132 | + $status = $this->repo->storeTemp( basename( $path ), $path ); |
| 133 | + if( ! $status->isOK() ) { |
| 134 | + // It is a convention in MediaWiki to only return one error per API exception, even if multiple errors |
| 135 | + // are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings. |
| 136 | + // This is a bit lame, as we may have more info in the $status and we're throwing it away, but to fix it means |
| 137 | + // redesigning API errors significantly. |
| 138 | + // $status->value just contains the virtual URL (if anything) which is probably useless to the caller |
| 139 | + $error = reset( $status->getErrorsArray() ); |
| 140 | + if ( ! count( $error ) ) { |
| 141 | + $error = reset( $status->getWarningsArray() ); |
| 142 | + if ( ! count( $error ) ) { |
| 143 | + $error = array( 'unknown', 'no error recorded' ); |
| 144 | + } |
| 145 | + } |
| 146 | + throw new UploadStashFileException( "error storing file in '$path': " . implode( '; ', $error ) ); |
| 147 | + } |
| 148 | + $stashPath = $status->value; |
| 149 | + |
| 150 | + // required info we always store. Must trump any other application info in $data |
| 151 | + // 'mTempPath', 'mFileSize', and 'mFileProps' are arbitrary names |
| 152 | + // chosen for compatibility with UploadBase's way of doing this. |
| 153 | + $requiredData = array( |
| 154 | + 'mTempPath' => $stashPath, |
| 155 | + 'mFileSize' => $fileProps['size'], |
| 156 | + 'mFileProps' => $fileProps, |
| 157 | + 'version' => UploadBase::SESSION_VERSION |
| 158 | + ); |
| 159 | + |
| 160 | + // now, merge required info and extra data into the session. (The extra data changes from application to application. |
| 161 | + // UploadWizard wants different things than say FirefoggChunkedUpload.) |
| 162 | + $_SESSION[UploadBase::SESSION_KEYNAME][$key] = array_merge( $data, $requiredData ); |
| 163 | + |
| 164 | + return $this->getFile( $key ); |
| 165 | + } |
| 166 | + |
| 167 | +} |
| 168 | + |
| 169 | +class UploadStashFile extends UnregisteredLocalFile { |
| 170 | + private $sessionStash; |
| 171 | + private $sessionKey; |
| 172 | + private $sessionData; |
| 173 | + private $urlName; |
| 174 | + |
| 175 | + /** |
| 176 | + * A LocalFile wrapper around a file that has been temporarily stashed, so we can do things like create thumbnails for it |
| 177 | + * Arguably UnregisteredLocalFile should be handling its own file repo but that class is a bit retarded currently |
| 178 | + * @param {UploadStash} $stash: UploadStash, useful for obtaining config, stashing transformed files |
| 179 | + * @param {FileRepo} $repo: repository where we should find the path |
| 180 | + * @param {String} $path: path to file |
| 181 | + * @param {String} $key: key to store the path and any stashed data under |
| 182 | + * @param {String} $data: any other data we want stored with this file |
| 183 | + * @throws UploadStashBadPathException |
| 184 | + * @throws UploadStashFileNotFoundException |
| 185 | + */ |
| 186 | + public function __construct( $stash, $repo, $path, $key, $data ) { |
| 187 | + $this->sessionStash = $stash; |
| 188 | + $this->sessionKey = $key; |
| 189 | + $this->sessionData = $data; |
| 190 | + |
| 191 | + // resolve mwrepo:// urls |
| 192 | + if ( $repo->isVirtualUrl( $path ) ) { |
| 193 | + $path = $repo->resolveVirtualUrl( $path ); |
| 194 | + } |
| 195 | + |
| 196 | + // check if path appears to be sane, no parent traversals, and is in this repo's temp zone. |
| 197 | + $repoTempPath = $repo->getZonePath( 'temp' ); |
| 198 | + if ( ( ! $repo->validateFilename( $path ) ) || |
| 199 | + ( strpos( $path, $repoTempPath ) !== 0 ) ) { |
| 200 | + throw new UploadStashBadPathException( "path '$path' is not valid or is not in repo temp area: '$repoTempPath'" ); |
| 201 | + } |
| 202 | + |
| 203 | + // check if path exists! and is a plain file. |
| 204 | + if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) { |
| 205 | + throw new UploadStashFileNotFoundException( "cannot find path '$path'" ); |
| 206 | + } |
| 207 | + |
| 208 | + parent::__construct( false, $repo, $path, false ); |
| 209 | + |
| 210 | + // we will be initializing from some tmpnam files that don't have extensions. |
| 211 | + // most of MediaWiki assumes all uploaded files have good extensions. So, we fix this. |
| 212 | + $this->name = basename( $this->path ); |
| 213 | + $this->setExtension(); |
| 214 | + |
| 215 | + } |
| 216 | + |
| 217 | + /** |
| 218 | + * A method needed by the file transforming and scaling routines in File.php |
| 219 | + * We do not necessarily care about doing the description at this point |
| 220 | + * However, we also can't return the empty string, as the rest of MediaWiki demands this (and calls to imagemagick |
| 221 | + * convert require it to be there) |
| 222 | + * @return {String} dummy value |
| 223 | + */ |
| 224 | + public function getDescriptionUrl() { |
| 225 | + return $this->getUrl(); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * Find or guess extension -- ensuring that our extension matches our mime type. |
| 230 | + * Since these files are constructed from php tempnames they may not start off |
| 231 | + * with an extension. |
| 232 | + * This does not override getExtension() because things like getMimeType() already call getExtension(), |
| 233 | + * and that results in infinite recursion. So, we preemptively *set* the extension so getExtension() can find it. |
| 234 | + * For obvious reasons this should be called as early as possible, as part of initialization |
| 235 | + */ |
| 236 | + public function setExtension() { |
| 237 | + // Does this have an extension? |
| 238 | + $n = strrpos( $this->path, '.' ); |
| 239 | + $extension = null; |
| 240 | + if ( $n !== false ) { |
| 241 | + $extension = $n ? substr( $this->path, $n + 1 ) : ''; |
| 242 | + } else { |
| 243 | + // If not, assume that it should be related to the mime type of the original file. |
| 244 | + // |
| 245 | + // This entire thing is backwards -- we *should* just create an extension based on |
| 246 | + // the mime type of the transformed file, *after* transformation. But File.php demands |
| 247 | + // to know the name of the transformed file before creating it. |
| 248 | + $mimeType = $this->getMimeType(); |
| 249 | + $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) ); |
| 250 | + if ( count( $extensions ) ) { |
| 251 | + $extension = $extensions[0]; |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + if ( is_null( $extension ) ) { |
| 256 | + throw new UploadStashFileException( "extension '$extension' is null" ); |
| 257 | + } |
| 258 | + |
| 259 | + $this->extension = parent::normalizeExtension( $extension ); |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Get the path for the thumbnail (actually any transformation of this file) |
| 264 | + * The actual argument is the result of thumbName although we seem to have |
| 265 | + * buggy code elsewhere that expects a boolean 'suffix' |
| 266 | + * |
| 267 | + * @param {String|false} $thumbName: name of thumbnail (e.g. "120px-123456.jpg" ), or false to just get the path |
| 268 | + * @return {String} path thumbnail should take on filesystem, or containing directory if thumbname is false |
| 269 | + */ |
| 270 | + public function getThumbPath( $thumbName = false ) { |
| 271 | + $path = dirname( $this->path ); |
| 272 | + if ( $thumbName !== false ) { |
| 273 | + $path .= "/$thumbName"; |
| 274 | + } |
| 275 | + return $path; |
| 276 | + } |
| 277 | + |
| 278 | + /** |
| 279 | + * Return the file/url base name of a thumbnail with the specified parameters |
| 280 | + * |
| 281 | + * @param {Array} $params: handler-specific parameters |
| 282 | + * @return {String|null} base name for URL, like '120px-12345.jpg', or null if there is no handler |
| 283 | + */ |
| 284 | + function thumbName( $params ) { |
| 285 | + if ( !$this->getHandler() ) { |
| 286 | + return null; |
| 287 | + } |
| 288 | + $extension = $this->getExtension(); |
| 289 | + list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); |
| 290 | + $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $this->getUrlName(); |
| 291 | + if ( $thumbExt != $extension ) { |
| 292 | + $thumbName .= ".$thumbExt"; |
| 293 | + } |
| 294 | + return $thumbName; |
| 295 | + } |
| 296 | + |
| 297 | + /** |
| 298 | + * Get a URL to access the thumbnail |
| 299 | + * This is required because the model of how files work requires that |
| 300 | + * the thumbnail urls be predictable. However, in our model the URL is not based on the filename |
| 301 | + * (that's hidden in the session) |
| 302 | + * |
| 303 | + * @param {String} $thumbName: basename of thumbnail file -- however, we don't want to use the file exactly |
| 304 | + * @return {String} URL to access thumbnail, or URL with partial path |
| 305 | + */ |
| 306 | + public function getThumbUrl( $thumbName = false ) { |
| 307 | + $path = $this->sessionStash->getBaseUrl(); |
| 308 | + if ( $thumbName !== false ) { |
| 309 | + $path .= '/' . rawurlencode( $thumbName ); |
| 310 | + } |
| 311 | + return $path; |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * The basename for the URL, which we want to not be related to the filename. |
| 316 | + * Will also be used as the lookup key for a thumbnail file. |
| 317 | + * @return {String} base url name, like '120px-123456.jpg' |
| 318 | + */ |
| 319 | + public function getUrlName() { |
| 320 | + if ( ! $this->urlName ) { |
| 321 | + $this->urlName = $this->sessionKey . '.' . $this->getExtension(); |
| 322 | + } |
| 323 | + return $this->urlName; |
| 324 | + } |
| 325 | + |
| 326 | + /** |
| 327 | + * Return the URL of the file, if for some reason we wanted to download it |
| 328 | + * We tend not to do this for the original file, but we do want thumb icons |
| 329 | + * @return {String} url |
| 330 | + */ |
| 331 | + public function getUrl() { |
| 332 | + if ( !isset( $this->url ) ) { |
| 333 | + $this->url = $this->sessionStash->getBaseUrl() . '/' . $this->getUrlName(); |
| 334 | + } |
| 335 | + return $this->url; |
| 336 | + } |
| 337 | + |
| 338 | + /** |
| 339 | + * Parent classes use this method, for no obvious reason, to return the path (relative to wiki root, I assume). |
| 340 | + * But with this class, the URL is unrelated to the path. |
| 341 | + * |
| 342 | + * @return {String} url |
| 343 | + */ |
| 344 | + public function getFullUrl() { |
| 345 | + return $this->getUrl(); |
| 346 | + } |
| 347 | + |
| 348 | + |
| 349 | + /** |
| 350 | + * Getter for session key (the session-unique id by which this file's location & metadata is stored in the session) |
| 351 | + * @return {String} session key |
| 352 | + */ |
| 353 | + public function getSessionKey() { |
| 354 | + return $this->sessionKey; |
| 355 | + } |
| 356 | + |
| 357 | + /** |
| 358 | + * Typically, transform() returns a ThumbnailImage, which you can think of as being the exact |
| 359 | + * equivalent of an HTML thumbnail on Wikipedia. So its URL is the full-size file, not the thumbnail's URL. |
| 360 | + * |
| 361 | + * Here we override transform() to stash the thumbnail file, and then |
| 362 | + * provide a way to get at the stashed thumbnail file to extract properties such as its URL |
| 363 | + * |
| 364 | + * @param {Array} $params: parameters suitable for File::transform() |
| 365 | + * @param {Bitmask} $flags: flags suitable for File::transform() |
| 366 | + * @return {ThumbnailImage} with additional File thumbnailFile property |
| 367 | + */ |
| 368 | + public function transform( $params, $flags = 0 ) { |
| 369 | + |
| 370 | + // force it to get a thumbnail right away |
| 371 | + $flags |= self::RENDER_NOW; |
| 372 | + |
| 373 | + // returns a ThumbnailImage object containing the url and path. Note. NOT A FILE OBJECT. |
| 374 | + $thumb = parent::transform( $params, $flags ); |
| 375 | + $key = $this->thumbName($params); |
| 376 | + |
| 377 | + // remove extension, so it's stored in the session under '120px-123456' |
| 378 | + // this makes it uniform with the other session key for the original, '123456' |
| 379 | + $n = strrpos( $key, '.' ); |
| 380 | + if ( $n !== false ) { |
| 381 | + $key = substr( $key, 0, $n ); |
| 382 | + } |
| 383 | + |
| 384 | + // stash the thumbnail File, and provide our caller with a way to get at its properties |
| 385 | + $stashedThumbFile = $this->sessionStash->stashFile( $thumb->path, array(), $key ); |
| 386 | + $thumb->thumbnailFile = $stashedThumbFile; |
| 387 | + |
| 388 | + return $thumb; |
| 389 | + |
| 390 | + } |
| 391 | + |
| 392 | + /** |
| 393 | + * Remove the associated temporary file |
| 394 | + * @return {Status} success |
| 395 | + */ |
| 396 | + public function remove() { |
| 397 | + return $this->repo->freeTemp( $this->path ); |
| 398 | + } |
| 399 | + |
| 400 | +} |
| 401 | + |
| 402 | +class UploadStashNotAvailableException extends MWException {}; |
| 403 | +class UploadStashFileNotFoundException extends MWException {}; |
| 404 | +class UploadStashBadPathException extends MWException {}; |
| 405 | +class UploadStashBadVersionException extends MWException {}; |
| 406 | +class UploadStashFileException extends MWException {}; |
| 407 | + |
Property changes on: trunk/phase3/includes/upload/UploadStash.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1 | 408 | Merged /branches/uploadwizard/extensions/includes/upload/UploadStash.php:r73550-74029 |
2 | 409 | Merged /branches/new-installer/phase3/includes/upload/UploadStash.php:r43664-66004 |
3 | 410 | Merged /branches/REL1_15/phase3/includes/upload/UploadStash.php:r51646 |
4 | 411 | Merged /branches/sqlite/includes/upload/UploadStash.php:r58211-58321 |
5 | 412 | Merged /trunk/phase3/includes/upload/UploadStash.php:r74019-75058,75060-75821 |
Added: svn:eol-style |
6 | 413 | + native |
Index: trunk/phase3/includes/upload/UploadFromFile.php |
— | — | @@ -52,4 +52,14 @@ |
53 | 53 | |
54 | 54 | return parent::verifyUpload(); |
55 | 55 | } |
| 56 | + |
| 57 | + /** |
| 58 | + * Get the path to the file underlying the upload |
| 59 | + * @return String path to file |
| 60 | + */ |
| 61 | + public function getFileTempname() { |
| 62 | + return $this->mUpload->getTempname(); |
| 63 | + } |
| 64 | + |
| 65 | + |
56 | 66 | } |
Property changes on: trunk/phase3/includes/upload/UploadFromFile.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
57 | 67 | Merged /branches/new-installer/phase3/includes/upload/UploadFromFile.php:r43664-66004 |
58 | 68 | Merged /branches/wmf-deployment/includes/upload/UploadFromFile.php:r53381 |
59 | 69 | Merged /branches/uploadwizard/phase3/includes/upload/UploadFromFile.php:r73550-75905 |
60 | 70 | Merged /branches/REL1_15/phase3/includes/upload/UploadFromFile.php:r51646 |
61 | 71 | Merged /branches/sqlite/includes/upload/UploadFromFile.php:r58211-58321 |
Index: trunk/phase3/includes/filerepo/File.php |
— | — | @@ -541,7 +541,7 @@ |
542 | 542 | * @param $params Array: an associative array of handler-specific parameters. |
543 | 543 | * Typical keys are width, height and page. |
544 | 544 | * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering |
545 | | - * @return MediaTransformOutput |
| 545 | + * @return MediaTransformOutput | false |
546 | 546 | */ |
547 | 547 | function transform( $params, $flags = 0 ) { |
548 | 548 | global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgServer; |
— | — | @@ -575,7 +575,7 @@ |
576 | 576 | $thumbPath = $this->getThumbPath( $thumbName ); |
577 | 577 | $thumbUrl = $this->getThumbUrl( $thumbName ); |
578 | 578 | |
579 | | - if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { |
| 579 | + if ( $this->repo && $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { |
580 | 580 | $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); |
581 | 581 | break; |
582 | 582 | } |
Property changes on: trunk/phase3/includes/filerepo/File.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
583 | 583 | Merged /branches/REL1_15/phase3/includes/filerepo/File.php:r51646 |
584 | 584 | Merged /branches/sqlite/includes/filerepo/File.php:r58211-58321 |
585 | 585 | Merged /branches/new-installer/phase3/includes/filerepo/File.php:r43664-66004 |
586 | 586 | Merged /branches/wmf-deployment/includes/filerepo/File.php:r53381 |
587 | 587 | Merged /branches/uploadwizard/phase3/includes/filerepo/File.php:r73550-75905 |
Index: trunk/phase3/includes/api/ApiQueryImageInfo.php |
— | — | @@ -36,8 +36,13 @@ |
37 | 37 | */ |
38 | 38 | class ApiQueryImageInfo extends ApiQueryBase { |
39 | 39 | |
40 | | - public function __construct( $query, $moduleName ) { |
41 | | - parent::__construct( $query, $moduleName, 'ii' ); |
| 40 | + public function __construct( $query, $moduleName, $prefix = 'ii' ) { |
| 41 | + // We allow a subclass to override the prefix, to create a related API module. |
| 42 | + // Some other parts of MediaWiki construct this with a null $prefix, which used to be ignored when this only took two arguments |
| 43 | + if ( is_null( $prefix ) ) { |
| 44 | + $prefix = 'ii'; |
| 45 | + } |
| 46 | + parent::__construct( $query, $moduleName, $prefix ); |
42 | 47 | } |
43 | 48 | |
44 | 49 | public function execute() { |
— | — | @@ -45,18 +50,8 @@ |
46 | 51 | |
47 | 52 | $prop = array_flip( $params['prop'] ); |
48 | 53 | |
49 | | - if ( $params['urlheight'] != - 1 && $params['urlwidth'] == - 1 ) { |
50 | | - $this->dieUsage( 'iiurlheight cannot be used without iiurlwidth', 'iiurlwidth' ); |
51 | | - } |
| 54 | + $scale = $this->getScale( $params ); |
52 | 55 | |
53 | | - if ( $params['urlwidth'] != - 1 ) { |
54 | | - $scale = array(); |
55 | | - $scale['width'] = $params['urlwidth']; |
56 | | - $scale['height'] = $params['urlheight']; |
57 | | - } else { |
58 | | - $scale = null; |
59 | | - } |
60 | | - |
61 | 56 | $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); |
62 | 57 | if ( !empty( $pageIds[NS_FILE] ) ) { |
63 | 58 | $titles = array_keys( $pageIds[NS_FILE] ); |
— | — | @@ -184,6 +179,28 @@ |
185 | 180 | } |
186 | 181 | |
187 | 182 | /** |
| 183 | + * From parameters, construct a 'scale' array |
| 184 | + * @param {Array} $params |
| 185 | + * @return {null|Array} key-val array of 'width' and 'height', or null |
| 186 | + */ |
| 187 | + public function getScale( $params ) { |
| 188 | + $p = $this->getModulePrefix(); |
| 189 | + if ( $params['urlheight'] != -1 && $params['urlwidth'] == -1 ) { |
| 190 | + $this->dieUsage( "${p}urlheight cannot be used without {$p}urlwidth", "{$p}urlwidth" ); |
| 191 | + } |
| 192 | + |
| 193 | + if ( $params['urlwidth'] != -1 ) { |
| 194 | + $scale = array(); |
| 195 | + $scale['width'] = $params['urlwidth']; |
| 196 | + $scale['height'] = $params['urlheight']; |
| 197 | + } else { |
| 198 | + $scale = null; |
| 199 | + } |
| 200 | + return $scale; |
| 201 | + } |
| 202 | + |
| 203 | + |
| 204 | + /** |
188 | 205 | * Get result information for an image revision |
189 | 206 | * |
190 | 207 | * @param $file File object |
— | — | @@ -324,11 +341,11 @@ |
325 | 342 | ), |
326 | 343 | 'urlwidth' => array( |
327 | 344 | ApiBase::PARAM_TYPE => 'integer', |
328 | | - ApiBase::PARAM_DFLT => - 1 |
| 345 | + ApiBase::PARAM_DFLT => -1 |
329 | 346 | ), |
330 | 347 | 'urlheight' => array( |
331 | 348 | ApiBase::PARAM_TYPE => 'integer', |
332 | | - ApiBase::PARAM_DFLT => - 1 |
| 349 | + ApiBase::PARAM_DFLT => -1 |
333 | 350 | ), |
334 | 351 | 'continue' => null, |
335 | 352 | ); |
— | — | @@ -356,6 +373,11 @@ |
357 | 374 | ); |
358 | 375 | } |
359 | 376 | |
| 377 | + |
| 378 | + /** |
| 379 | + * Return the API documentation for the parameters. |
| 380 | + * @return {Array} parameter documentation. |
| 381 | + */ |
360 | 382 | public function getParamDescription() { |
361 | 383 | $p = $this->getModulePrefix(); |
362 | 384 | return array( |
— | — | @@ -375,14 +397,14 @@ |
376 | 398 | ' metadata - Lists EXIF metadata for the version of the image', |
377 | 399 | ' archivename - Adds the file name of the archive version for non-latest versions', |
378 | 400 | ' bitdepth - Adds the bit depth of the version', |
379 | | - ), |
| 401 | + ), |
| 402 | + 'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", |
| 403 | + 'Only the current version of the image can be scaled' ), |
| 404 | + 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", |
380 | 405 | 'limit' => 'How many image revisions to return', |
381 | 406 | 'start' => 'Timestamp to start listing from', |
382 | 407 | 'end' => 'Timestamp to stop listing at', |
383 | | - 'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", |
384 | | - 'Only the current version of the image can be scaled' ), |
385 | | - 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", |
386 | | - 'continue' => 'When more results are available, use this to continue', |
| 408 | + 'continue' => 'If the query response includes a continue value, use it here to get another page of results' |
387 | 409 | ); |
388 | 410 | } |
389 | 411 | |
Property changes on: trunk/phase3/includes/api/ApiQueryImageInfo.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
390 | 412 | Merged /branches/uploadwizard/phase3/includes/api/ApiQueryImageInfo.php:r73550-75905 |
391 | 413 | Merged /branches/REL1_15/phase3/includes/api/ApiQueryImageInfo.php:r51646 |
392 | 414 | Merged /branches/REL1_16/phase3/includes/api/ApiQueryImageInfo.php:r63621-63636,69357 |
393 | 415 | Merged /branches/wmf/1.16wmf4/includes/api/ApiQueryImageInfo.php:r69521 |
394 | 416 | Merged /branches/sqlite/includes/api/ApiQueryImageInfo.php:r58211-58321 |
395 | 417 | Merged /branches/wmf-deployment/includes/api/ApiQueryImageInfo.php:r53381,59952 |
Index: trunk/phase3/includes/api/ApiUpload.php |
— | — | @@ -80,25 +80,66 @@ |
81 | 81 | // Check permission to upload this file |
82 | 82 | $permErrors = $this->mUpload->verifyPermissions( $wgUser ); |
83 | 83 | if ( $permErrors !== true ) { |
84 | | - // Todo: stash the upload and allow choosing a new name |
| 84 | + // TODO: stash the upload and allow choosing a new name |
85 | 85 | $this->dieUsageMsg( array( 'badaccess-groups' ) ); |
86 | 86 | } |
87 | 87 | |
88 | | - // Check warnings if necessary |
89 | | - $warnings = $this->checkForWarnings(); |
90 | | - if ( $warnings ) { |
91 | | - $this->getResult()->addValue( null, $this->getModuleName(), $warnings ); |
| 88 | + // Prepare the API result |
| 89 | + $result = array(); |
| 90 | + |
| 91 | + $warnings = $this->getApiWarnings(); |
| 92 | + if ( $warnings ) { |
| 93 | + $result['result'] = 'Warning'; |
| 94 | + $result['warnings'] = $warnings; |
| 95 | + // in case the warnings can be fixed with some further user action, let's stash this upload |
| 96 | + // and return a key they can use to restart it |
| 97 | + try { |
| 98 | + $result['sessionkey'] = $this->performStash(); |
| 99 | + } catch ( MWException $e ) { |
| 100 | + $result['warnings']['stashfailed'] = $e->getMessage(); |
| 101 | + } |
| 102 | + } elseif ( $this->mParams['stash'] ) { |
| 103 | + // Some uploads can request they be stashed, so as not to publish them immediately. |
| 104 | + // In this case, a failure to stash ought to be fatal |
| 105 | + try { |
| 106 | + $result['result'] = 'Success'; |
| 107 | + $result['sessionkey'] = $this->performStash(); |
| 108 | + } catch ( MWException $e ) { |
| 109 | + $this->dieUsage( $e->getMessage(), 'stashfailed' ); |
| 110 | + } |
92 | 111 | } else { |
93 | | - // Perform the upload |
| 112 | + // This is the most common case -- a normal upload with no warnings |
| 113 | + // $result will be formatted properly for the API already, with a status |
94 | 114 | $result = $this->performUpload(); |
95 | | - $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
96 | 115 | } |
97 | 116 | |
| 117 | + if ( $result['result'] === 'Success' ) { |
| 118 | + $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); |
| 119 | + } |
| 120 | + |
| 121 | + $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
| 122 | + |
98 | 123 | // Cleanup any temporary mess |
99 | 124 | $this->mUpload->cleanupTempFile(); |
100 | 125 | } |
101 | 126 | |
102 | 127 | /** |
| 128 | + * Stash the file and return the session key |
| 129 | + * Also re-raises exceptions with slightly more informative message strings (useful for API) |
| 130 | + * @throws MWException |
| 131 | + * @return {String} session key |
| 132 | + */ |
| 133 | + function performStash() { |
| 134 | + try { |
| 135 | + $sessionKey = $this->mUpload->stashSessionFile()->getSessionKey(); |
| 136 | + } catch ( MWException $e ) { |
| 137 | + throw new MWException( 'Stashing temporary file failed: ' . get_class($e) . ' ' . $e->getMessage() ); |
| 138 | + } |
| 139 | + return $sessionKey; |
| 140 | + } |
| 141 | + |
| 142 | + |
| 143 | + /** |
103 | 144 | * Select an upload module and set it to mUpload. Dies on failure. If the |
104 | 145 | * request was a status request and not a true upload, returns false; |
105 | 146 | * otherwise true |
— | — | @@ -106,13 +147,14 @@ |
107 | 148 | * @return bool |
108 | 149 | */ |
109 | 150 | protected function selectUploadModule() { |
| 151 | + global $wgAllowAsyncCopyUploads; |
110 | 152 | $request = $this->getMain()->getRequest(); |
111 | 153 | |
112 | 154 | // One and only one of the following parameters is needed |
113 | 155 | $this->requireOnlyOneParameter( $this->mParams, |
114 | 156 | 'sessionkey', 'file', 'url', 'statuskey' ); |
115 | 157 | |
116 | | - if ( isset( $this->mParams['statuskey'] ) ) { |
| 158 | + if ( $wgAllowAsyncCopyUploads && $this->mParams['statuskey'] ) { |
117 | 159 | // Status request for an async upload |
118 | 160 | $sessionData = UploadFromUrlJob::getSessionData( $this->mParams['statuskey'] ); |
119 | 161 | if ( !isset( $sessionData['result'] ) ) { |
— | — | @@ -126,12 +168,14 @@ |
127 | 169 | return false; |
128 | 170 | |
129 | 171 | } |
130 | | - |
| 172 | + |
| 173 | + |
131 | 174 | // The following modules all require the filename parameter to be set |
132 | 175 | if ( is_null( $this->mParams['filename'] ) ) { |
133 | 176 | $this->dieUsageMsg( array( 'missingparam', 'filename' ) ); |
134 | 177 | } |
135 | | - |
| 178 | + |
| 179 | + |
136 | 180 | if ( $this->mParams['sessionkey'] ) { |
137 | 181 | // Upload stashed in a previous request |
138 | 182 | $sessionData = $request->getSessionData( UploadBase::getSessionKeyName() ); |
— | — | @@ -249,56 +293,41 @@ |
250 | 294 | } |
251 | 295 | } |
252 | 296 | |
| 297 | + |
253 | 298 | /** |
254 | 299 | * Check warnings if ignorewarnings is not set. |
255 | | - * Returns a suitable result array if there were warnings |
| 300 | + * Returns a suitable array for inclusion into API results if there were warnings |
| 301 | + * Returns the empty array if there were no warnings |
| 302 | + * |
| 303 | + * @return array |
256 | 304 | */ |
257 | | - protected function checkForWarnings() { |
258 | | - $result = array(); |
| 305 | + protected function getApiWarnings() { |
| 306 | + $warnings = array(); |
259 | 307 | |
260 | 308 | if ( !$this->mParams['ignorewarnings'] ) { |
261 | 309 | $warnings = $this->mUpload->checkWarnings(); |
262 | 310 | if ( $warnings ) { |
263 | | - $result['result'] = 'Warning'; |
264 | | - $result['warnings'] = $this->transformWarnings( $warnings ); |
| 311 | + // Add indices |
| 312 | + $this->getResult()->setIndexedTagName( $warnings, 'warning' ); |
265 | 313 | |
266 | | - $sessionKey = $this->mUpload->stashSession(); |
267 | | - if ( !$sessionKey ) { |
268 | | - $this->dieUsage( 'Stashing temporary file failed', 'stashfailed' ); |
| 314 | + if ( isset( $warnings['duplicate'] ) ) { |
| 315 | + $dupes = array(); |
| 316 | + foreach ( $warnings['duplicate'] as $dupe ) { |
| 317 | + $dupes[] = $dupe->getName(); |
| 318 | + } |
| 319 | + $this->getResult()->setIndexedTagName( $dupes, 'duplicate' ); |
| 320 | + $warnings['duplicate'] = $dupes; |
269 | 321 | } |
270 | 322 | |
271 | | - $result['sessionkey'] = $sessionKey; |
272 | | - |
273 | | - return $result; |
| 323 | + if ( isset( $warnings['exists'] ) ) { |
| 324 | + $warning = $warnings['exists']; |
| 325 | + unset( $warnings['exists'] ); |
| 326 | + $warnings[$warning['warning']] = $warning['file']->getName(); |
| 327 | + } |
274 | 328 | } |
275 | 329 | } |
276 | | - return; |
277 | | - } |
278 | | - |
279 | | - /** |
280 | | - * Transforms a warnings array returned by mUpload->checkWarnings() into |
281 | | - * something that can be directly used as API result |
282 | | - */ |
283 | | - protected function transformWarnings( $warnings ) { |
284 | | - // Add indices |
285 | | - $this->getResult()->setIndexedTagName( $warnings, 'warning' ); |
286 | 330 | |
287 | | - if ( isset( $warnings['duplicate'] ) ) { |
288 | | - $dupes = array(); |
289 | | - foreach ( $warnings['duplicate'] as $dupe ) { |
290 | | - $dupes[] = $dupe->getName(); |
291 | | - } |
292 | | - $this->getResult()->setIndexedTagName( $dupes, 'duplicate' ); |
293 | | - $warnings['duplicate'] = $dupes; |
294 | | - } |
295 | | - |
296 | | - if ( isset( $warnings['exists'] ) ) { |
297 | | - $warning = $warnings['exists']; |
298 | | - unset( $warnings['exists'] ); |
299 | | - $warnings[$warning['warning']] = $warning['file']->getName(); |
300 | | - } |
301 | | - |
302 | | - return $warnings; |
| 331 | + return $warnings; |
303 | 332 | } |
304 | 333 | |
305 | 334 | /** |
— | — | @@ -346,8 +375,8 @@ |
347 | 376 | |
348 | 377 | $result['result'] = 'Success'; |
349 | 378 | $result['filename'] = $file->getName(); |
350 | | - $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); |
351 | 379 | |
| 380 | + |
352 | 381 | return $result; |
353 | 382 | } |
354 | 383 | |
— | — | @@ -384,8 +413,8 @@ |
385 | 414 | 'ignorewarnings' => false, |
386 | 415 | 'file' => null, |
387 | 416 | 'url' => null, |
388 | | - |
389 | 417 | 'sessionkey' => null, |
| 418 | + 'stash' => false, |
390 | 419 | ); |
391 | 420 | |
392 | 421 | global $wgAllowAsyncCopyUploads; |
— | — | @@ -410,7 +439,8 @@ |
411 | 440 | 'ignorewarnings' => 'Ignore any warnings', |
412 | 441 | 'file' => 'File contents', |
413 | 442 | 'url' => 'Url to fetch the file from', |
414 | | - 'sessionkey' => 'Session key returned by a previous upload that failed due to warnings', |
| 443 | + 'sessionkey' => 'Session key that identifies a previous upload that was stashed temporarily.', |
| 444 | + 'stash' => 'If set, the server will not add the file to the repository and stash it temporarily.' |
415 | 445 | ); |
416 | 446 | |
417 | 447 | global $wgAllowAsyncCopyUploads; |
Property changes on: trunk/phase3/includes/api/ApiUpload.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
418 | 448 | Merged /branches/uploadwizard/phase3/includes/api/ApiUpload.php:r73550-75905 |
419 | 449 | Merged /branches/REL1_15/phase3/includes/api/ApiUpload.php:r51646 |
420 | 450 | Merged /branches/REL1_16/phase3/includes/api/ApiUpload.php:r63621-63636,69357 |
421 | 451 | Merged /branches/wmf/1.16wmf4/includes/api/ApiUpload.php:r69521 |
422 | 452 | Merged /branches/sqlite/includes/api/ApiUpload.php:r58211-58321 |
423 | 453 | Merged /branches/wmf-deployment/includes/api/ApiUpload.php:r53381,59952 |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -634,6 +634,7 @@ |
635 | 635 | 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php', |
636 | 636 | 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', |
637 | 637 | 'SpecialSearch' => 'includes/specials/SpecialSearch.php', |
| 638 | + 'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php', |
638 | 639 | 'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php', |
639 | 640 | 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', |
640 | 641 | 'SpecialTags' => 'includes/specials/SpecialTags.php', |
— | — | @@ -669,6 +670,7 @@ |
670 | 671 | 'UserloginTemplate' => 'includes/templates/Userlogin.php', |
671 | 672 | |
672 | 673 | # includes/upload |
| 674 | + 'UploadStash' => 'includes/upload/UploadStash.php', |
673 | 675 | 'UploadBase' => 'includes/upload/UploadBase.php', |
674 | 676 | 'UploadFromStash' => 'includes/upload/UploadFromStash.php', |
675 | 677 | 'UploadFromFile' => 'includes/upload/UploadFromFile.php', |
Property changes on: trunk/phase3/includes/AutoLoader.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
676 | 678 | Merged /branches/sqlite/includes/AutoLoader.php:r58211-58321 |
677 | 679 | Merged /branches/new-installer/phase3/includes/AutoLoader.php:r43664-66004 |
678 | 680 | Merged /branches/wmf-deployment/includes/AutoLoader.php:r53381 |
679 | 681 | Merged /branches/uploadwizard/phase3/includes/AutoLoader.php:r73550-75905 |
680 | 682 | Merged /branches/REL1_15/phase3/includes/AutoLoader.php:r51646 |
Index: trunk/phase3/includes/specials/SpecialUploadStash.php |
— | — | @@ -0,0 +1,140 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Special:UploadStash |
| 5 | + * |
| 6 | + * Web access for files temporarily stored by UploadStash. |
| 7 | + * |
| 8 | + * For example -- files that were uploaded with the UploadWizard extension are stored temporarily |
| 9 | + * before committing them to the db. But we want to see their thumbnails and get other information |
| 10 | + * about them. |
| 11 | + * |
| 12 | + * Since this is based on the user's session, in effect this creates a private temporary file area. |
| 13 | + * However, the URLs for the files cannot be shared. |
| 14 | + * |
| 15 | + * @file |
| 16 | + * @ingroup SpecialPage |
| 17 | + * @ingroup Upload |
| 18 | + */ |
| 19 | + |
| 20 | +class SpecialUploadStash extends SpecialPage { |
| 21 | + |
| 22 | + static $HttpErrors = array( // FIXME: Use OutputPage::getStatusMessage() --RK |
| 23 | + 400 => 'Bad Request', |
| 24 | + 403 => 'Access Denied', |
| 25 | + 404 => 'File not found', |
| 26 | + 500 => 'Internal Server Error', |
| 27 | + ); |
| 28 | + |
| 29 | + // UploadStash |
| 30 | + private $stash; |
| 31 | + |
| 32 | + // we should not be reading in really big files and serving them out |
| 33 | + private $maxServeFileSize = 262144; // 256K |
| 34 | + |
| 35 | + // $request is the request (usually wgRequest) |
| 36 | + // $subpage is everything in the URL after Special:UploadStash |
| 37 | + // FIXME: These parameters don't match SpecialPage::__construct()'s params at all, and are unused --RK |
| 38 | + public function __construct( $request = null, $subpage = null ) { |
| 39 | + parent::__construct( 'UploadStash', 'upload' ); |
| 40 | + $this->stash = new UploadStash(); |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * If file available in stash, cats it out to the client as a simple HTTP response. |
| 45 | + * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. |
| 46 | + * |
| 47 | + * @param {String} $subPage: subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part |
| 48 | + * @return {Boolean} success |
| 49 | + */ |
| 50 | + public function execute( $subPage ) { |
| 51 | + global $wgOut, $wgUser; |
| 52 | + |
| 53 | + if ( !$this->userCanExecute( $wgUser ) ) { |
| 54 | + $this->displayRestrictionError(); |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + // prevent callers from doing standard HTML output -- we'll take it from here |
| 59 | + $wgOut->disable(); |
| 60 | + |
| 61 | + try { |
| 62 | + $file = $this->getStashFile( $subPage ); |
| 63 | + if ( $file->getSize() > $this->maxServeFileSize ) { |
| 64 | + throw new MWException( 'file size too large' ); |
| 65 | + } |
| 66 | + $this->outputFile( $file ); |
| 67 | + return true; |
| 68 | + |
| 69 | + } catch( UploadStashFileNotFoundException $e ) { |
| 70 | + $code = 404; |
| 71 | + } catch( UploadStashBadPathException $e ) { |
| 72 | + $code = 403; |
| 73 | + } catch( Exception $e ) { |
| 74 | + $code = 500; |
| 75 | + } |
| 76 | + |
| 77 | + wfHttpError( $code, self::$HttpErrors[$code], $e->getCode(), $e->getMessage() ); |
| 78 | + return false; |
| 79 | + } |
| 80 | + |
| 81 | + |
| 82 | + /** |
| 83 | + * Convert the incoming url portion (subpage of Special page) into a stashed file, if available. |
| 84 | + * @param {String} $subPage |
| 85 | + * @return {File} file object |
| 86 | + * @throws MWException, UploadStashFileNotFoundException, UploadStashBadPathException |
| 87 | + */ |
| 88 | + private function getStashFile( $subPage ) { |
| 89 | + // due to an implementation quirk (and trying to be compatible with older method) |
| 90 | + // the stash key doesn't have an extension |
| 91 | + $key = $subPage; |
| 92 | + $n = strrpos( $subPage, '.' ); |
| 93 | + if ( $n !== false ) { |
| 94 | + $key = $n ? substr( $subPage, 0, $n ) : $subPage; |
| 95 | + } |
| 96 | + |
| 97 | + try { |
| 98 | + $file = $this->stash->getFile( $key ); |
| 99 | + } catch ( UploadStashFileNotFoundException $e ) { |
| 100 | + // if we couldn't find it, and it looks like a thumbnail, |
| 101 | + // and it looks like we have the original, go ahead and generate it |
| 102 | + $matches = array(); |
| 103 | + if ( ! preg_match( '/^(\d+)px-(.*)$/', $key, $matches ) ) { |
| 104 | + // that doesn't look like a thumbnail. re-raise exception |
| 105 | + throw $e; |
| 106 | + } |
| 107 | + |
| 108 | + list( $dummy, $width, $origKey ) = $matches; |
| 109 | + |
| 110 | + // do not trap exceptions, if key is in bad format, or file not found, |
| 111 | + // let exceptions propagate to caller. |
| 112 | + $origFile = $this->stash->getFile( $origKey ); |
| 113 | + |
| 114 | + // ok we're here so the original must exist. Generate the thumbnail. |
| 115 | + // because the file is a UploadStashFile, this thumbnail will also be stashed, |
| 116 | + // and a thumbnailFile will be created in the thumbnailImage composite object |
| 117 | + $thumbnailImage = null; |
| 118 | + if ( !( $thumbnailImage = $origFile->getThumbnail( $width ) ) ) { |
| 119 | + throw new MWException( 'Could not obtain thumbnail' ); |
| 120 | + } |
| 121 | + $file = $thumbnailImage->thumbnailFile; |
| 122 | + } |
| 123 | + |
| 124 | + return $file; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Output HTTP response for file |
| 129 | + * Side effects, obviously, of echoing lots of stuff to stdout. |
| 130 | + * @param {File} file |
| 131 | + */ |
| 132 | + private function outputFile( $file ) { |
| 133 | + header( 'Content-Type: ' . $file->getMimeType(), true ); |
| 134 | + header( 'Content-Transfer-Encoding: binary', true ); |
| 135 | + header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true ); |
| 136 | + header( 'Pragma: public', true ); |
| 137 | + header( 'Content-Length: ' . $file->getSize(), true ); // FIXME: PHP can handle Content-Length for you just fine --RK |
| 138 | + readfile( $file->getPath() ); |
| 139 | + } |
| 140 | +} |
| 141 | + |
Property changes on: trunk/phase3/includes/specials/SpecialUploadStash.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
1 | 142 | Merged /branches/wmf-deployment/includes/specials/SpecialUploadStash.php:r53381,56967 |
2 | 143 | Merged /branches/REL1_15/phase3/includes/specials/SpecialUploadStash.php:r51646 |
3 | 144 | Merged /branches/sqlite/includes/specials/SpecialUploadStash.php:r58211-58321 |
4 | 145 | Merged /trunk/phase3/includes/specials/SpecialUploadStash.php:r73549-75058,75060-75821 |
Added: svn:eol-style |
5 | 146 | + native |
Index: trunk/phase3/includes/SpecialPage.php |
— | — | @@ -149,6 +149,7 @@ |
150 | 150 | 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), |
151 | 151 | 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ), |
152 | 152 | 'Upload' => 'SpecialUpload', |
| 153 | + 'UploadStash' => 'SpecialUploadStash', |
153 | 154 | |
154 | 155 | # Wiki data and tools |
155 | 156 | 'Statistics' => 'SpecialStatistics', |
Property changes on: trunk/phase3/includes/SpecialPage.php |
___________________________________________________________________ |
Added: svn:mergeinfo |
156 | 157 | Merged /branches/new-installer/phase3/includes/SpecialPage.php:r43664-66004 |
157 | 158 | Merged /branches/wmf-deployment/includes/SpecialPage.php:r53381 |
158 | 159 | Merged /branches/uploadwizard/phase3/includes/SpecialPage.php:r73550-75905 |
159 | 160 | Merged /branches/REL1_15/phase3/includes/SpecialPage.php:r51646 |
160 | 161 | Merged /branches/sqlite/includes/SpecialPage.php:r58211-58321 |