1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\HttpFoundation\Tests; 13 14use Symfony\Component\HttpFoundation\BinaryFileResponse; 15use Symfony\Component\HttpFoundation\File\Stream; 16use Symfony\Component\HttpFoundation\Request; 17use Symfony\Component\HttpFoundation\ResponseHeaderBag; 18use Symfony\Component\HttpFoundation\Tests\File\FakeFile; 19 20class BinaryFileResponseTest extends ResponseTestCase 21{ 22 public function testConstruction() 23 { 24 $file = __DIR__.'/../README.md'; 25 $response = new BinaryFileResponse($file, 404, ['X-Header' => 'Foo'], true, null, true, true); 26 $this->assertEquals(404, $response->getStatusCode()); 27 $this->assertEquals('Foo', $response->headers->get('X-Header')); 28 $this->assertTrue($response->headers->has('ETag')); 29 $this->assertTrue($response->headers->has('Last-Modified')); 30 $this->assertFalse($response->headers->has('Content-Disposition')); 31 32 $response = BinaryFileResponse::create($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE); 33 $this->assertEquals(404, $response->getStatusCode()); 34 $this->assertFalse($response->headers->has('ETag')); 35 $this->assertEquals('inline; filename="README.md"', $response->headers->get('Content-Disposition')); 36 } 37 38 public function testConstructWithNonAsciiFilename() 39 { 40 touch(sys_get_temp_dir().'/fööö.html'); 41 42 $response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, [], true, 'attachment'); 43 44 @unlink(sys_get_temp_dir().'/fööö.html'); 45 46 $this->assertSame('fööö.html', $response->getFile()->getFilename()); 47 } 48 49 public function testSetContent() 50 { 51 $this->expectException('LogicException'); 52 $response = new BinaryFileResponse(__FILE__); 53 $response->setContent('foo'); 54 } 55 56 public function testGetContent() 57 { 58 $response = new BinaryFileResponse(__FILE__); 59 $this->assertFalse($response->getContent()); 60 } 61 62 public function testSetContentDispositionGeneratesSafeFallbackFilename() 63 { 64 $response = new BinaryFileResponse(__FILE__); 65 $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'föö.html'); 66 67 $this->assertSame('attachment; filename="f__.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition')); 68 } 69 70 public function testSetContentDispositionGeneratesSafeFallbackFilenameForWronglyEncodedFilename() 71 { 72 $response = new BinaryFileResponse(__FILE__); 73 74 $iso88591EncodedFilename = utf8_decode('föö.html'); 75 $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $iso88591EncodedFilename); 76 77 // the parameter filename* is invalid in this case (rawurldecode('f%F6%F6') does not provide a UTF-8 string but an ISO-8859-1 encoded one) 78 $this->assertSame('attachment; filename="f__.html"; filename*=utf-8\'\'f%F6%F6.html', $response->headers->get('Content-Disposition')); 79 } 80 81 /** 82 * @dataProvider provideRanges 83 */ 84 public function testRequests($requestRange, $offset, $length, $responseRange) 85 { 86 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); 87 88 // do a request to get the ETag 89 $request = Request::create('/'); 90 $response->prepare($request); 91 $etag = $response->headers->get('ETag'); 92 93 // prepare a request for a range of the testing file 94 $request = Request::create('/'); 95 $request->headers->set('If-Range', $etag); 96 $request->headers->set('Range', $requestRange); 97 98 $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); 99 fseek($file, $offset); 100 $data = fread($file, $length); 101 fclose($file); 102 103 $this->expectOutputString($data); 104 $response = clone $response; 105 $response->prepare($request); 106 $response->sendContent(); 107 108 $this->assertEquals(206, $response->getStatusCode()); 109 $this->assertEquals($responseRange, $response->headers->get('Content-Range')); 110 $this->assertSame((string) $length, $response->headers->get('Content-Length')); 111 } 112 113 /** 114 * @dataProvider provideRanges 115 */ 116 public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange) 117 { 118 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); 119 120 // do a request to get the LastModified 121 $request = Request::create('/'); 122 $response->prepare($request); 123 $lastModified = $response->headers->get('Last-Modified'); 124 125 // prepare a request for a range of the testing file 126 $request = Request::create('/'); 127 $request->headers->set('If-Range', $lastModified); 128 $request->headers->set('Range', $requestRange); 129 130 $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); 131 fseek($file, $offset); 132 $data = fread($file, $length); 133 fclose($file); 134 135 $this->expectOutputString($data); 136 $response = clone $response; 137 $response->prepare($request); 138 $response->sendContent(); 139 140 $this->assertEquals(206, $response->getStatusCode()); 141 $this->assertEquals($responseRange, $response->headers->get('Content-Range')); 142 } 143 144 public function provideRanges() 145 { 146 return [ 147 ['bytes=1-4', 1, 4, 'bytes 1-4/35'], 148 ['bytes=-5', 30, 5, 'bytes 30-34/35'], 149 ['bytes=30-', 30, 5, 'bytes 30-34/35'], 150 ['bytes=30-30', 30, 1, 'bytes 30-30/35'], 151 ['bytes=30-34', 30, 5, 'bytes 30-34/35'], 152 ['bytes=30-40', 30, 5, 'bytes 30-34/35'], 153 ]; 154 } 155 156 public function testRangeRequestsWithoutLastModifiedDate() 157 { 158 // prevent auto last modified 159 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false); 160 161 // prepare a request for a range of the testing file 162 $request = Request::create('/'); 163 $request->headers->set('If-Range', date('D, d M Y H:i:s').' GMT'); 164 $request->headers->set('Range', 'bytes=1-4'); 165 166 $this->expectOutputString(file_get_contents(__DIR__.'/File/Fixtures/test.gif')); 167 $response = clone $response; 168 $response->prepare($request); 169 $response->sendContent(); 170 171 $this->assertEquals(200, $response->getStatusCode()); 172 $this->assertNull($response->headers->get('Content-Range')); 173 } 174 175 /** 176 * @dataProvider provideFullFileRanges 177 */ 178 public function testFullFileRequests($requestRange) 179 { 180 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); 181 182 // prepare a request for a range of the testing file 183 $request = Request::create('/'); 184 $request->headers->set('Range', $requestRange); 185 186 $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); 187 $data = fread($file, 35); 188 fclose($file); 189 190 $this->expectOutputString($data); 191 $response = clone $response; 192 $response->prepare($request); 193 $response->sendContent(); 194 195 $this->assertEquals(200, $response->getStatusCode()); 196 } 197 198 public function provideFullFileRanges() 199 { 200 return [ 201 ['bytes=0-'], 202 ['bytes=0-34'], 203 ['bytes=-35'], 204 // Syntactical invalid range-request should also return the full resource 205 ['bytes=20-10'], 206 ['bytes=50-40'], 207 // range units other than bytes must be ignored 208 ['unknown=10-20'], 209 ]; 210 } 211 212 public function testRangeOnPostMethod() 213 { 214 $request = Request::create('/', 'POST'); 215 $request->headers->set('Range', 'bytes=10-20'); 216 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); 217 218 $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); 219 $data = fread($file, 35); 220 fclose($file); 221 222 $this->expectOutputString($data); 223 $response = clone $response; 224 $response->prepare($request); 225 $response->sendContent(); 226 227 $this->assertSame(200, $response->getStatusCode()); 228 $this->assertSame('35', $response->headers->get('Content-Length')); 229 $this->assertNull($response->headers->get('Content-Range')); 230 } 231 232 public function testUnpreparedResponseSendsFullFile() 233 { 234 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200); 235 236 $data = file_get_contents(__DIR__.'/File/Fixtures/test.gif'); 237 238 $this->expectOutputString($data); 239 $response = clone $response; 240 $response->sendContent(); 241 242 $this->assertEquals(200, $response->getStatusCode()); 243 } 244 245 /** 246 * @dataProvider provideInvalidRanges 247 */ 248 public function testInvalidRequests($requestRange) 249 { 250 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); 251 252 // prepare a request for a range of the testing file 253 $request = Request::create('/'); 254 $request->headers->set('Range', $requestRange); 255 256 $response = clone $response; 257 $response->prepare($request); 258 $response->sendContent(); 259 260 $this->assertEquals(416, $response->getStatusCode()); 261 $this->assertEquals('bytes */35', $response->headers->get('Content-Range')); 262 } 263 264 public function provideInvalidRanges() 265 { 266 return [ 267 ['bytes=-40'], 268 ['bytes=40-50'], 269 ]; 270 } 271 272 /** 273 * @dataProvider provideXSendfileFiles 274 */ 275 public function testXSendfile($file) 276 { 277 $request = Request::create('/'); 278 $request->headers->set('X-Sendfile-Type', 'X-Sendfile'); 279 280 BinaryFileResponse::trustXSendfileTypeHeader(); 281 $response = BinaryFileResponse::create($file, 200, ['Content-Type' => 'application/octet-stream']); 282 $response->prepare($request); 283 284 $this->expectOutputString(''); 285 $response->sendContent(); 286 287 $this->assertStringContainsString('README.md', $response->headers->get('X-Sendfile')); 288 } 289 290 public function provideXSendfileFiles() 291 { 292 return [ 293 [__DIR__.'/../README.md'], 294 ['file://'.__DIR__.'/../README.md'], 295 ]; 296 } 297 298 /** 299 * @dataProvider getSampleXAccelMappings 300 */ 301 public function testXAccelMapping($realpath, $mapping, $virtual) 302 { 303 $request = Request::create('/'); 304 $request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect'); 305 $request->headers->set('X-Accel-Mapping', $mapping); 306 307 $file = new FakeFile($realpath, __DIR__.'/File/Fixtures/test'); 308 309 BinaryFileResponse::trustXSendfileTypeHeader(); 310 $response = new BinaryFileResponse($file, 200, ['Content-Type' => 'application/octet-stream']); 311 $reflection = new \ReflectionObject($response); 312 $property = $reflection->getProperty('file'); 313 $property->setAccessible(true); 314 $property->setValue($response, $file); 315 316 $response->prepare($request); 317 $this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect')); 318 } 319 320 public function testDeleteFileAfterSend() 321 { 322 $request = Request::create('/'); 323 324 $path = __DIR__.'/File/Fixtures/to_delete'; 325 touch($path); 326 $realPath = realpath($path); 327 $this->assertFileExists($realPath); 328 329 $response = new BinaryFileResponse($realPath, 200, ['Content-Type' => 'application/octet-stream']); 330 $response->deleteFileAfterSend(true); 331 332 $response->prepare($request); 333 $response->sendContent(); 334 335 $this->assertFileDoesNotExist($path); 336 } 337 338 public function testAcceptRangeOnUnsafeMethods() 339 { 340 $request = Request::create('/', 'POST'); 341 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); 342 $response->prepare($request); 343 344 $this->assertEquals('none', $response->headers->get('Accept-Ranges')); 345 } 346 347 public function testAcceptRangeNotOverriden() 348 { 349 $request = Request::create('/', 'POST'); 350 $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); 351 $response->headers->set('Accept-Ranges', 'foo'); 352 $response->prepare($request); 353 354 $this->assertEquals('foo', $response->headers->get('Accept-Ranges')); 355 } 356 357 public function getSampleXAccelMappings() 358 { 359 return [ 360 ['/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'], 361 ['/home/foo/bar.txt', '/var/www/=/files/,/home/foo/=/baz/', '/baz/bar.txt'], 362 ['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null], 363 ]; 364 } 365 366 public function testStream() 367 { 368 $request = Request::create('/'); 369 $response = new BinaryFileResponse(new Stream(__DIR__.'/../README.md'), 200, ['Content-Type' => 'text/plain']); 370 $response->prepare($request); 371 372 $this->assertNull($response->headers->get('Content-Length')); 373 } 374 375 protected function provideResponse() 376 { 377 return new BinaryFileResponse(__DIR__.'/../README.md', 200, ['Content-Type' => 'application/octet-stream']); 378 } 379 380 public static function tearDownAfterClass() 381 { 382 $path = __DIR__.'/../Fixtures/to_delete'; 383 if (file_exists($path)) { 384 @unlink($path); 385 } 386 } 387} 388