diff --git a/core/ProxyHttp.php b/core/ProxyHttp.php index 5952089eda911ca6b477ab5aea3e7523017def87..c500b40fdf5db38b89c86efeb685f35aa8d60588 100644 --- a/core/ProxyHttp.php +++ b/core/ProxyHttp.php @@ -56,8 +56,13 @@ class ProxyHttp * @param string $contentType The content type of the static file. * @param bool $expireFarFuture Day in the far future to set the Expires header to. * Should be set to false for files that should not be cached. + * @param int|false $byteStart The starting byte in the file to serve. If false, the data from the beginning + * of the file will be served. + * @param int|false $byteEnd The ending byte in the file to serve. If false, the data from $byteStart to the + * end of the file will be served. */ - public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100) + public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100, $byteStart = false, + $byteEnd = false) { // if the file cannot be found return HTTP status code '404' if (!file_exists($file)) { @@ -65,7 +70,6 @@ class ProxyHttp return; } - // conditional GET $modifiedSince = Http::getModifiedSinceHeader(); $fileModifiedTime = @filemtime($file); @@ -88,6 +92,14 @@ class ProxyHttp } // if we have to serve the file, serve it now, either in the clear or compressed + if ($byteStart === false) { + $byteStart = 0; + } + + if ($byteEnd === false) { + $byteEnd = filesize($file); + } + $compressed = false; $encoding = ''; $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file); @@ -102,11 +114,14 @@ class ProxyHttp // compress the file if it doesn't exist or is newer than the existing cached file, and cache // the compressed result if (self::shouldCompressFile($file, $filegz)) { - self::compressFile($file, $filegz, $encoding); + self::compressFile($file, $filegz, $encoding, $byteStart, $byteEnd); } $compressed = true; $file = $filegz; + + $byteStart = 0; + $byteEnd = filesize($file); } } else { // if a compressed file exists, the file was manually compressed so we just serve that @@ -115,6 +130,9 @@ class ProxyHttp ) { $compressed = true; $file = $filegz; + + $byteStart = 0; + $byteEnd = filesize($file); } } } @@ -122,7 +140,7 @@ class ProxyHttp @header('Last-Modified: ' . $lastModified); if (!$phpOutputCompressionEnabled) { - @header('Content-Length: ' . filesize($file)); + @header('Content-Length: ' . ($byteEnd - $byteStart)); } if (!empty($contentType)) { @@ -133,7 +151,7 @@ class ProxyHttp @header('Content-Encoding: ' . $encoding); } - if (!_readfile($file)) { + if (!_readfile($file, $byteStart, $byteEnd)) { self::setHttpStatus('505 Internal server error'); } } @@ -248,9 +266,11 @@ class ProxyHttp return !file_exists($compressedFilePath) || ($toCompressLastModified > $compressedLastModified); } - private static function compressFile($fileToCompress, $compressedFilePath, $compressionEncoding) + private static function compressFile($fileToCompress, $compressedFilePath, $compressionEncoding, $byteStart, + $byteEnd) { $data = file_get_contents($fileToCompress); + $data = substr($data, $byteStart, $byteEnd - $byteStart); if ($compressionEncoding == 'deflate') { $data = gzdeflate($data, 9); diff --git a/libs/upgradephp/upgrade.php b/libs/upgradephp/upgrade.php index 5ab68078a8323536b486b97db113b8a4a13d67a7..7d2317247084cf873f84e50bc733a484497c67fa 100644 --- a/libs/upgradephp/upgrade.php +++ b/libs/upgradephp/upgrade.php @@ -612,20 +612,27 @@ function safe_unserialize( $str ) * @param resource $context * @return int the number of bytes read from the file, or false if an error occurs */ -function _readfile($filename, $useIncludePath = false, $context = null) +function _readfile($filename, $byteStart, $byteEnd, $useIncludePath = false, $context = null) { $count = @filesize($filename); // built-in function has a 2 MB limit when using mmap - if (function_exists('readfile') && $count <= (2 * 1024 * 1024)) { + if (function_exists('readfile') + && $count <= (2 * 1024 * 1024) + && $byteStart == 0 + && $byteEnd == $count + ) { return @readfile($filename, $useIncludePath, $context); } // when in doubt (or when readfile() function is disabled) $handle = @fopen($filename, SettingsServer::isWindows() ? "rb" : "r"); if ($handle) { - while(!feof($handle)) { - echo fread($handle, 8192); + fseek($handle, $byteStart); + + for ($pos = $byteStart; $pos < $byteEnd && !feof($handle); $pos = ftell($handle)) { + echo fread($handle, min(8192, $byteEnd - $pos)); + ob_flush(); flush(); } diff --git a/tests/PHPUnit/Core/ServeStaticFileTest.php b/tests/PHPUnit/Core/ServeStaticFileTest.php index b3536b7aa3dfdd35c193311e409eca2bfd369695..42aab636d4cae45a0ba73573902f929cd44f3f7f 100644 --- a/tests/PHPUnit/Core/ServeStaticFileTest.php +++ b/tests/PHPUnit/Core/ServeStaticFileTest.php @@ -36,8 +36,16 @@ define("UNIT_TEST_MODE", "unitTestMode"); define("NULL_FILE_SRV_MODE", "nullFile"); define("GHOST_FILE_SRV_MODE", "ghostFile"); define("TEST_FILE_SRV_MODE", "testFile"); +define("PARTIAL_TEST_FILE_SRV_MODE", "partialTestFile"); +define("WHOLE_TEST_FILE_WITH_RANGE_SRV_MODE", "wholeTestFileWithRange"); + +define("PARTIAL_BYTE_START", 1204); +define("PARTIAL_BYTE_END", 14724); // If the static file server has not been requested, the standard unit test case class is defined +/** + * @group ServeStaticFileTest + */ class Test_Piwik_ServeStaticFile extends PHPUnit_Framework_TestCase { public function tearDown() @@ -388,6 +396,90 @@ class Test_Piwik_ServeStaticFile extends PHPUnit_Framework_TestCase $this->removeCompressedFiles(); } + /** + * @group Core + */ + public function test_partialFileServeNoCompression() + { + $this->removeCompressedFiles(); + + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $this->getPartialTestFileSrvModeUrl()); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + $partialResponse = curl_exec($curlHandle); + $responseInfo = curl_getinfo($curlHandle); + curl_close($curlHandle); + + clearstatcache(); + + // check no compressed files created + $this->assertFalse(file_exists($this->getCompressedFileLocation() . ".deflate")); + $this->assertFalse(file_exists($this->getCompressedFileLocation() . ".gz")); + + // check $partialResponse + $this->assertEquals(PARTIAL_BYTE_END - PARTIAL_BYTE_START, $responseInfo["size_download"]); + + $expectedPartialContents = substr(file_get_contents(TEST_FILE_LOCATION), PARTIAL_BYTE_START, + PARTIAL_BYTE_END - PARTIAL_BYTE_START); + $this->assertEquals($expectedPartialContents, $partialResponse); + } + + /** + * @group Core + */ + public function test_partialFileServeWithCompression() + { + $this->removeCompressedFiles(); + + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $this->getPartialTestFileSrvModeUrl()); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlHandle, CURLOPT_ENCODING, "deflate"); + $partialResponse = curl_exec($curlHandle); + $responseInfo = curl_getinfo($curlHandle); + curl_close($curlHandle); + + clearstatcache(); + + // check the correct compressed file is created + $this->assertTrue(file_exists($this->getCompressedFileLocation() . ".deflate")); + $this->assertFalse(file_exists($this->getCompressedFileLocation() . ".gz")); + + // check $partialResponse + $expectedPartialContents = substr(file_get_contents(TEST_FILE_LOCATION), PARTIAL_BYTE_START, + PARTIAL_BYTE_END - PARTIAL_BYTE_START); + $this->assertEquals($expectedPartialContents, $partialResponse); + + $this->removeCompressedFiles(); + } + + /** + * @group Core + */ + public function test_wholeFileServeWithByteRange() + { + $this->removeCompressedFiles(); + + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $this->getWholeTestFileWithRangeSrvModeUrl()); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlHandle, CURLOPT_ENCODING, "deflate"); + $fullResponse = curl_exec($curlHandle); + $responseInfo = curl_getinfo($curlHandle); + curl_close($curlHandle); + + clearstatcache(); + + // check the correct compressed file is created + $this->assertTrue(file_exists($this->getCompressedFileLocation() . ".deflate")); + $this->assertFalse(file_exists($this->getCompressedFileLocation() . ".gz")); + + // check $fullResponse + $this->assertEquals(file_get_contents(TEST_FILE_LOCATION), $fullResponse); + + $this->removeCompressedFiles(); + } + /** * Helper methods */ @@ -415,6 +507,16 @@ class Test_Piwik_ServeStaticFile extends PHPUnit_Framework_TestCase return $this->getStaticSrvUrl() . TEST_FILE_SRV_MODE; } + private function getPartialTestFileSrvModeUrl() + { + return $this->getStaticSrvUrl() . PARTIAL_TEST_FILE_SRV_MODE; + } + + private function getWholeTestFileWithRangeSrvModeUrl() + { + return $this->getStaticSrvUrl() . WHOLE_TEST_FILE_WITH_RANGE_SRV_MODE; + } + private function setZlibOutputRequest($url) { return $url . "&" . ZLIB_OUTPUT_REQUEST_VAR . "=1"; diff --git a/tests/resources/staticFileServer.php b/tests/resources/staticFileServer.php index cdaa54830f859ad4b25a0898e7362ae1cb040562..69ed020696a392b01d3ff8e304f6a0cc06cab182 100644 --- a/tests/resources/staticFileServer.php +++ b/tests/resources/staticFileServer.php @@ -54,6 +54,11 @@ define("ZLIB_OUTPUT_REQUEST_VAR", "zlibOutput"); define("NULL_FILE_SRV_MODE", "nullFile"); define("GHOST_FILE_SRV_MODE", "ghostFile"); define("TEST_FILE_SRV_MODE", "testFile"); +define("PARTIAL_TEST_FILE_SRV_MODE", "partialTestFile"); +define("WHOLE_TEST_FILE_WITH_RANGE_SRV_MODE", "wholeTestFileWithRange"); + +define("PARTIAL_BYTE_START", 1204); +define("PARTIAL_BYTE_END", 14724); /** @@ -89,4 +94,14 @@ switch ($staticFileServerMode) { ProxyHttp::serverStaticFile(TEST_FILE_LOCATION, TEST_FILE_CONTENT_TYPE); break; + + case PARTIAL_TEST_FILE_SRV_MODE: + + ProxyHttp::serverStaticFile(TEST_FILE_LOCATION, TEST_FILE_CONTENT_TYPE, $expireFarFutureDays = 100, PARTIAL_BYTE_START, PARTIAL_BYTE_END); + break; + + case WHOLE_TEST_FILE_WITH_RANGE_SRV_MODE: + + ProxyHttp::serverStaticFile(TEST_FILE_LOCATION, TEST_FILE_CONTENT_TYPE, $expireFarFutureDays = 100, 0, filesize(TEST_FILE_LOCATION)); + break; } \ No newline at end of file