/core/ProxyHttp.php
PHP | 232 lines | 121 code | 28 blank | 83 comment | 40 complexity | 7c61f5c4dfb2bf242eb6fdb9b9c0dcdb MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
- <?php
- /**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
- namespace Piwik;
- /**
- * Http helper: static file server proxy, with compression, caching, isHttps() helper...
- *
- * Used to server piwik.js and the merged+minified CSS and JS files
- *
- */
- class ProxyHttp
- {
- /**
- * Returns true if the current request appears to be a secure HTTPS connection
- *
- * @return bool
- */
- public static function isHttps()
- {
- return Url::getCurrentScheme() === 'https';
- }
- /**
- * Serve static files through php proxy.
- *
- * It performs the following actions:
- * - Checks the file is readable or returns "HTTP/1.0 404 Not Found"
- * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE
- * with the modification date of the static file
- * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in
- * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file
- * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension.
- * Using manually compressed static files requires you to manually update the compressed file when
- * the static file is updated.
- * - Overrides server cache control config to allow caching
- * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according
- * to users encoding capacities.
- *
- * Warning:
- * Compressed filed are stored in the /tmp directory.
- * If this method is used with two files bearing the same name but located in different locations,
- * there is a risk of conflict. One file could be served with the content of the other.
- * A future upgrade of this method would be to recreate the directory structure of the static file
- * within a /tmp/compressed-static-files directory.
- *
- * @param string $file The location of the static file to serve
- * @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.
- */
- public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100)
- {
- if (file_exists($file)) {
- // conditional GET
- $modifiedSince = '';
- if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
- $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
- // strip any trailing data appended to header
- if (false !== ($semicolon = strpos($modifiedSince, ';'))) {
- $modifiedSince = substr($modifiedSince, 0, $semicolon);
- }
- }
- $fileModifiedTime = @filemtime($file);
- $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT';
- // set HTTP response headers
- self::overrideCacheControlHeaders('public');
- @header('Vary: Accept-Encoding');
- @header('Content-Disposition: inline; filename=' . basename($file));
- if ($expireFarFutureDays) {
- // Required by proxy caches potentially in between the browser and server to cache the request indeed
- @header("Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * (int)$expireFarFutureDays) . ' GMT');
- }
- // Returns 304 if not modified since
- if ($modifiedSince === $lastModified) {
- self::setHttpStatus('304 Not Modified');
- } else {
- // optional compression
- $compressed = false;
- $encoding = '';
- $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file);
- $phpOutputCompressionEnabled = ProxyHttp::isPhpOutputCompressed();
- if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) {
- $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
- if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents')) {
- if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches)) {
- $encoding = 'deflate';
- $filegz = $compressedFileLocation . '.deflate';
- } else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches)) {
- $encoding = $matches[1];
- $filegz = $compressedFileLocation . '.gz';
- }
- if (!empty($encoding)) {
- // compress-on-demand and use cache
- if (!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz))) {
- $data = file_get_contents($file);
- if ($encoding == 'deflate') {
- $data = gzdeflate($data, 9);
- } else if ($encoding == 'gzip' || $encoding == 'x-gzip') {
- $data = gzencode($data, 9);
- }
- file_put_contents($filegz, $data);
- }
- $compressed = true;
- $file = $filegz;
- }
- } else {
- // manually compressed
- $filegz = $compressedFileLocation . '.gz';
- if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz))) {
- $encoding = $matches[1];
- $compressed = true;
- $file = $filegz;
- }
- }
- }
- @header('Last-Modified: ' . $lastModified);
- if (!$phpOutputCompressionEnabled) {
- @header('Content-Length: ' . filesize($file));
- }
- if (!empty($contentType)) {
- @header('Content-Type: ' . $contentType);
- }
- if ($compressed) {
- @header('Content-Encoding: ' . $encoding);
- }
- if (!_readfile($file)) {
- self::setHttpStatus('505 Internal server error');
- }
- }
- } else {
- self::setHttpStatus('404 Not Found');
- }
- }
- /**
- * Test if php output is compressed
- *
- * @return bool True if php output is (or suspected/likely) to be compressed
- */
- public static function isPhpOutputCompressed()
- {
- // Off = ''; On = '1'; otherwise, it's a buffer size
- $zlibOutputCompression = ini_get('zlib.output_compression');
- // could be ob_gzhandler, ob_deflatehandler, etc
- $outputHandler = ini_get('output_handler');
- // output handlers can be stacked
- $obHandlers = array_filter(ob_list_handlers(), function ($var) {
- return $var !== "default output handler";
- });
- // user defined handler via wrapper
- if (!defined('PIWIK_TEST_MODE')) {
- $autoPrependFile = ini_get('auto_prepend_file');
- $autoAppendFile = ini_get('auto_append_file');
- }
- return !empty($zlibOutputCompression) ||
- !empty($outputHandler) ||
- !empty($obHandlers) ||
- !empty($autoPrependFile) ||
- !empty($autoAppendFile);
- }
- /**
- * Workaround IE bug when downloading certain document types over SSL and
- * cache control headers are present, e.g.,
- *
- * Cache-Control: no-cache
- * Cache-Control: no-store,max-age=0,must-revalidate
- * Pragma: no-cache
- *
- * @see http://support.microsoft.com/kb/316431/
- * @see RFC2616
- *
- * @param string $override One of "public", "private", "no-cache", or "no-store". (optional)
- */
- public static function overrideCacheControlHeaders($override = null)
- {
- if ($override || self::isHttps()) {
- @header('Pragma: ');
- @header('Expires: ');
- if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) {
- @header("Cache-Control: $override, must-revalidate");
- } else {
- @header('Cache-Control: must-revalidate');
- }
- }
- }
- /**
- * Set response header, e.g., HTTP/1.0 200 Ok
- *
- * @param string $status Status
- * @return bool
- */
- protected static function setHttpStatus($status)
- {
- if (strpos(PHP_SAPI, '-fcgi') === false) {
- @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status);
- } else {
- // FastCGI
- @header('Status: ' . $status);
- }
- }
- }