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\Request;
16use Symfony\Component\HttpFoundation\ResponseHeaderBag;
17use Symfony\Component\HttpFoundation\Tests\File\FakeFile;
18
19class BinaryFileResponseTest extends ResponseTestCase
20{
21    public function testConstruction()
22    {
23        $file = __DIR__.'/../README.md';
24        $response = new BinaryFileResponse($file, 404, array('X-Header' => 'Foo'), true, null, true, true);
25        $this->assertEquals(404, $response->getStatusCode());
26        $this->assertEquals('Foo', $response->headers->get('X-Header'));
27        $this->assertTrue($response->headers->has('ETag'));
28        $this->assertTrue($response->headers->has('Last-Modified'));
29        $this->assertFalse($response->headers->has('Content-Disposition'));
30
31        $response = BinaryFileResponse::create($file, 404, array(), true, ResponseHeaderBag::DISPOSITION_INLINE);
32        $this->assertEquals(404, $response->getStatusCode());
33        $this->assertFalse($response->headers->has('ETag'));
34        $this->assertEquals('inline; filename="README.md"', $response->headers->get('Content-Disposition'));
35    }
36
37    public function testConstructWithNonAsciiFilename()
38    {
39        touch(sys_get_temp_dir().'/fööö.html');
40
41        $response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, array(), true, 'attachment');
42
43        @unlink(sys_get_temp_dir().'/fööö.html');
44
45        $this->assertSame('fööö.html', $response->getFile()->getFilename());
46    }
47
48    /**
49     * @expectedException \LogicException
50     */
51    public function testSetContent()
52    {
53        $response = new BinaryFileResponse(__FILE__);
54        $response->setContent('foo');
55    }
56
57    public function testGetContent()
58    {
59        $response = new BinaryFileResponse(__FILE__);
60        $this->assertFalse($response->getContent());
61    }
62
63    public function testSetContentDispositionGeneratesSafeFallbackFilename()
64    {
65        $response = new BinaryFileResponse(__FILE__);
66        $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'föö.html');
67
68        $this->assertSame('attachment; filename="f__.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition'));
69    }
70
71    /**
72     * @dataProvider provideRanges
73     */
74    public function testRequests($requestRange, $offset, $length, $responseRange)
75    {
76        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag();
77
78        // do a request to get the ETag
79        $request = Request::create('/');
80        $response->prepare($request);
81        $etag = $response->headers->get('ETag');
82
83        // prepare a request for a range of the testing file
84        $request = Request::create('/');
85        $request->headers->set('If-Range', $etag);
86        $request->headers->set('Range', $requestRange);
87
88        $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
89        fseek($file, $offset);
90        $data = fread($file, $length);
91        fclose($file);
92
93        $this->expectOutputString($data);
94        $response = clone $response;
95        $response->prepare($request);
96        $response->sendContent();
97
98        $this->assertEquals(206, $response->getStatusCode());
99        $this->assertEquals($responseRange, $response->headers->get('Content-Range'));
100    }
101
102    /**
103     * @dataProvider provideRanges
104     */
105    public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange)
106    {
107        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'));
108
109        // do a request to get the LastModified
110        $request = Request::create('/');
111        $response->prepare($request);
112        $lastModified = $response->headers->get('Last-Modified');
113
114        // prepare a request for a range of the testing file
115        $request = Request::create('/');
116        $request->headers->set('If-Range', $lastModified);
117        $request->headers->set('Range', $requestRange);
118
119        $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
120        fseek($file, $offset);
121        $data = fread($file, $length);
122        fclose($file);
123
124        $this->expectOutputString($data);
125        $response = clone $response;
126        $response->prepare($request);
127        $response->sendContent();
128
129        $this->assertEquals(206, $response->getStatusCode());
130        $this->assertEquals($responseRange, $response->headers->get('Content-Range'));
131    }
132
133    public function provideRanges()
134    {
135        return array(
136            array('bytes=1-4', 1, 4, 'bytes 1-4/35'),
137            array('bytes=-5', 30, 5, 'bytes 30-34/35'),
138            array('bytes=30-', 30, 5, 'bytes 30-34/35'),
139            array('bytes=30-30', 30, 1, 'bytes 30-30/35'),
140            array('bytes=30-34', 30, 5, 'bytes 30-34/35'),
141        );
142    }
143
144    public function testRangeRequestsWithoutLastModifiedDate()
145    {
146        // prevent auto last modified
147        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'), true, null, false, false);
148
149        // prepare a request for a range of the testing file
150        $request = Request::create('/');
151        $request->headers->set('If-Range', date('D, d M Y H:i:s').' GMT');
152        $request->headers->set('Range', 'bytes=1-4');
153
154        $this->expectOutputString(file_get_contents(__DIR__.'/File/Fixtures/test.gif'));
155        $response = clone $response;
156        $response->prepare($request);
157        $response->sendContent();
158
159        $this->assertEquals(200, $response->getStatusCode());
160        $this->assertNull($response->headers->get('Content-Range'));
161    }
162
163    /**
164     * @dataProvider provideFullFileRanges
165     */
166    public function testFullFileRequests($requestRange)
167    {
168        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag();
169
170        // prepare a request for a range of the testing file
171        $request = Request::create('/');
172        $request->headers->set('Range', $requestRange);
173
174        $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
175        $data = fread($file, 35);
176        fclose($file);
177
178        $this->expectOutputString($data);
179        $response = clone $response;
180        $response->prepare($request);
181        $response->sendContent();
182
183        $this->assertEquals(200, $response->getStatusCode());
184    }
185
186    public function provideFullFileRanges()
187    {
188        return array(
189            array('bytes=0-'),
190            array('bytes=0-34'),
191            array('bytes=-35'),
192            // Syntactical invalid range-request should also return the full resource
193            array('bytes=20-10'),
194            array('bytes=50-40'),
195        );
196    }
197
198    /**
199     * @dataProvider provideInvalidRanges
200     */
201    public function testInvalidRequests($requestRange)
202    {
203        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag();
204
205        // prepare a request for a range of the testing file
206        $request = Request::create('/');
207        $request->headers->set('Range', $requestRange);
208
209        $response = clone $response;
210        $response->prepare($request);
211        $response->sendContent();
212
213        $this->assertEquals(416, $response->getStatusCode());
214        $this->assertEquals('bytes */35', $response->headers->get('Content-Range'));
215    }
216
217    public function provideInvalidRanges()
218    {
219        return array(
220            array('bytes=-40'),
221            array('bytes=30-40'),
222        );
223    }
224
225    /**
226     * @dataProvider provideXSendfileFiles
227     */
228    public function testXSendfile($file)
229    {
230        $request = Request::create('/');
231        $request->headers->set('X-Sendfile-Type', 'X-Sendfile');
232
233        BinaryFileResponse::trustXSendfileTypeHeader();
234        $response = BinaryFileResponse::create($file, 200, array('Content-Type' => 'application/octet-stream'));
235        $response->prepare($request);
236
237        $this->expectOutputString('');
238        $response->sendContent();
239
240        $this->assertContains('README.md', $response->headers->get('X-Sendfile'));
241    }
242
243    public function provideXSendfileFiles()
244    {
245        return array(
246            array(__DIR__.'/../README.md'),
247            array('file://'.__DIR__.'/../README.md'),
248        );
249    }
250
251    /**
252     * @dataProvider getSampleXAccelMappings
253     */
254    public function testXAccelMapping($realpath, $mapping, $virtual)
255    {
256        $request = Request::create('/');
257        $request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect');
258        $request->headers->set('X-Accel-Mapping', $mapping);
259
260        $file = new FakeFile($realpath, __DIR__.'/File/Fixtures/test');
261
262        BinaryFileResponse::trustXSendfileTypeHeader();
263        $response = new BinaryFileResponse($file, 200, array('Content-Type' => 'application/octet-stream'));
264        $reflection = new \ReflectionObject($response);
265        $property = $reflection->getProperty('file');
266        $property->setAccessible(true);
267        $property->setValue($response, $file);
268
269        $response->prepare($request);
270        $this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect'));
271    }
272
273    public function testDeleteFileAfterSend()
274    {
275        $request = Request::create('/');
276
277        $path = __DIR__.'/File/Fixtures/to_delete';
278        touch($path);
279        $realPath = realpath($path);
280        $this->assertFileExists($realPath);
281
282        $response = new BinaryFileResponse($realPath, 200, array('Content-Type' => 'application/octet-stream'));
283        $response->deleteFileAfterSend(true);
284
285        $response->prepare($request);
286        $response->sendContent();
287
288        $this->assertFileNotExists($path);
289    }
290
291    public function testAcceptRangeOnUnsafeMethods()
292    {
293        $request = Request::create('/', 'POST');
294        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'));
295        $response->prepare($request);
296
297        $this->assertEquals('none', $response->headers->get('Accept-Ranges'));
298    }
299
300    public function testAcceptRangeNotOverriden()
301    {
302        $request = Request::create('/', 'POST');
303        $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'));
304        $response->headers->set('Accept-Ranges', 'foo');
305        $response->prepare($request);
306
307        $this->assertEquals('foo', $response->headers->get('Accept-Ranges'));
308    }
309
310    public function getSampleXAccelMappings()
311    {
312        return array(
313            array('/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'),
314            array('/home/foo/bar.txt', '/var/www/=/files/,/home/foo/=/baz/', '/baz/bar.txt'),
315        );
316    }
317
318    protected function provideResponse()
319    {
320        return new BinaryFileResponse(__DIR__.'/../README.md', 200, array('Content-Type' => 'application/octet-stream'));
321    }
322
323    public static function tearDownAfterClass()
324    {
325        $path = __DIR__.'/../Fixtures/to_delete';
326        if (file_exists($path)) {
327            @unlink($path);
328        }
329    }
330}
331