PageRenderTime 49ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/core/ProxyHttp.php

https://github.com/CodeYellowBV/piwik
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
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. /**
  11. * Http helper: static file server proxy, with compression, caching, isHttps() helper...
  12. *
  13. * Used to server piwik.js and the merged+minified CSS and JS files
  14. *
  15. */
  16. class ProxyHttp
  17. {
  18. /**
  19. * Returns true if the current request appears to be a secure HTTPS connection
  20. *
  21. * @return bool
  22. */
  23. public static function isHttps()
  24. {
  25. return Url::getCurrentScheme() === 'https';
  26. }
  27. /**
  28. * Serve static files through php proxy.
  29. *
  30. * It performs the following actions:
  31. * - Checks the file is readable or returns "HTTP/1.0 404 Not Found"
  32. * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE
  33. * with the modification date of the static file
  34. * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in
  35. * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file
  36. * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension.
  37. * Using manually compressed static files requires you to manually update the compressed file when
  38. * the static file is updated.
  39. * - Overrides server cache control config to allow caching
  40. * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according
  41. * to users encoding capacities.
  42. *
  43. * Warning:
  44. * Compressed filed are stored in the /tmp directory.
  45. * If this method is used with two files bearing the same name but located in different locations,
  46. * there is a risk of conflict. One file could be served with the content of the other.
  47. * A future upgrade of this method would be to recreate the directory structure of the static file
  48. * within a /tmp/compressed-static-files directory.
  49. *
  50. * @param string $file The location of the static file to serve
  51. * @param string $contentType The content type of the static file.
  52. * @param bool $expireFarFuture Day in the far future to set the Expires header to.
  53. * Should be set to false for files that should not be cached.
  54. */
  55. public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100)
  56. {
  57. if (file_exists($file)) {
  58. // conditional GET
  59. $modifiedSince = '';
  60. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  61. $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  62. // strip any trailing data appended to header
  63. if (false !== ($semicolon = strpos($modifiedSince, ';'))) {
  64. $modifiedSince = substr($modifiedSince, 0, $semicolon);
  65. }
  66. }
  67. $fileModifiedTime = @filemtime($file);
  68. $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT';
  69. // set HTTP response headers
  70. self::overrideCacheControlHeaders('public');
  71. @header('Vary: Accept-Encoding');
  72. @header('Content-Disposition: inline; filename=' . basename($file));
  73. if ($expireFarFutureDays) {
  74. // Required by proxy caches potentially in between the browser and server to cache the request indeed
  75. @header("Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * (int)$expireFarFutureDays) . ' GMT');
  76. }
  77. // Returns 304 if not modified since
  78. if ($modifiedSince === $lastModified) {
  79. self::setHttpStatus('304 Not Modified');
  80. } else {
  81. // optional compression
  82. $compressed = false;
  83. $encoding = '';
  84. $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file);
  85. $phpOutputCompressionEnabled = ProxyHttp::isPhpOutputCompressed();
  86. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) {
  87. $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
  88. if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents')) {
  89. if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches)) {
  90. $encoding = 'deflate';
  91. $filegz = $compressedFileLocation . '.deflate';
  92. } else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches)) {
  93. $encoding = $matches[1];
  94. $filegz = $compressedFileLocation . '.gz';
  95. }
  96. if (!empty($encoding)) {
  97. // compress-on-demand and use cache
  98. if (!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz))) {
  99. $data = file_get_contents($file);
  100. if ($encoding == 'deflate') {
  101. $data = gzdeflate($data, 9);
  102. } else if ($encoding == 'gzip' || $encoding == 'x-gzip') {
  103. $data = gzencode($data, 9);
  104. }
  105. file_put_contents($filegz, $data);
  106. }
  107. $compressed = true;
  108. $file = $filegz;
  109. }
  110. } else {
  111. // manually compressed
  112. $filegz = $compressedFileLocation . '.gz';
  113. if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz))) {
  114. $encoding = $matches[1];
  115. $compressed = true;
  116. $file = $filegz;
  117. }
  118. }
  119. }
  120. @header('Last-Modified: ' . $lastModified);
  121. if (!$phpOutputCompressionEnabled) {
  122. @header('Content-Length: ' . filesize($file));
  123. }
  124. if (!empty($contentType)) {
  125. @header('Content-Type: ' . $contentType);
  126. }
  127. if ($compressed) {
  128. @header('Content-Encoding: ' . $encoding);
  129. }
  130. if (!_readfile($file)) {
  131. self::setHttpStatus('505 Internal server error');
  132. }
  133. }
  134. } else {
  135. self::setHttpStatus('404 Not Found');
  136. }
  137. }
  138. /**
  139. * Test if php output is compressed
  140. *
  141. * @return bool True if php output is (or suspected/likely) to be compressed
  142. */
  143. public static function isPhpOutputCompressed()
  144. {
  145. // Off = ''; On = '1'; otherwise, it's a buffer size
  146. $zlibOutputCompression = ini_get('zlib.output_compression');
  147. // could be ob_gzhandler, ob_deflatehandler, etc
  148. $outputHandler = ini_get('output_handler');
  149. // output handlers can be stacked
  150. $obHandlers = array_filter(ob_list_handlers(), function ($var) {
  151. return $var !== "default output handler";
  152. });
  153. // user defined handler via wrapper
  154. if (!defined('PIWIK_TEST_MODE')) {
  155. $autoPrependFile = ini_get('auto_prepend_file');
  156. $autoAppendFile = ini_get('auto_append_file');
  157. }
  158. return !empty($zlibOutputCompression) ||
  159. !empty($outputHandler) ||
  160. !empty($obHandlers) ||
  161. !empty($autoPrependFile) ||
  162. !empty($autoAppendFile);
  163. }
  164. /**
  165. * Workaround IE bug when downloading certain document types over SSL and
  166. * cache control headers are present, e.g.,
  167. *
  168. * Cache-Control: no-cache
  169. * Cache-Control: no-store,max-age=0,must-revalidate
  170. * Pragma: no-cache
  171. *
  172. * @see http://support.microsoft.com/kb/316431/
  173. * @see RFC2616
  174. *
  175. * @param string $override One of "public", "private", "no-cache", or "no-store". (optional)
  176. */
  177. public static function overrideCacheControlHeaders($override = null)
  178. {
  179. if ($override || self::isHttps()) {
  180. @header('Pragma: ');
  181. @header('Expires: ');
  182. if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) {
  183. @header("Cache-Control: $override, must-revalidate");
  184. } else {
  185. @header('Cache-Control: must-revalidate');
  186. }
  187. }
  188. }
  189. /**
  190. * Set response header, e.g., HTTP/1.0 200 Ok
  191. *
  192. * @param string $status Status
  193. * @return bool
  194. */
  195. protected static function setHttpStatus($status)
  196. {
  197. if (strpos(PHP_SAPI, '-fcgi') === false) {
  198. @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status);
  199. } else {
  200. // FastCGI
  201. @header('Status: ' . $status);
  202. }
  203. }
  204. }