PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / ProxyHttp.php
matomo / app / core Last commit date
API 2 weeks ago Access 2 weeks ago Application 2 weeks ago Archive 2 weeks ago ArchiveProcessor 2 weeks ago Archiver 2 years ago AssetManager 2 weeks ago Auth 2 weeks ago Category 2 weeks ago Changes 2 months ago CliMulti 2 weeks ago Columns 2 weeks ago Concurrency 2 weeks ago Config 2 weeks ago Container 2 months ago CronArchive 2 weeks ago DataAccess 2 weeks ago DataFiles 2 years ago DataTable 2 weeks ago Db 2 weeks ago DeviceDetector 1 year ago Email 2 years ago Exception 5 months ago Http 2 weeks ago Intl 4 months ago Log 2 years ago Mail 1 year ago Measurable 8 months ago Menu 2 weeks ago Metrics 2 weeks ago Notification 8 months ago Period 2 weeks ago Plugin 2 weeks ago Policy 2 months ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 2 weeks ago Request 2 weeks ago Scheduler 2 weeks ago Segment 2 weeks ago Session 1 month ago Settings 2 weeks ago Tracker 2 weeks ago Translation 2 months ago Twig 1 year ago UpdateCheck 4 months ago Updater 2 weeks ago Updates 1 month ago Validators 1 year ago View 2 months ago ViewDataTable 2 weeks ago Visualization 2 weeks ago Widget 2 weeks ago .htaccess 2 years ago Access.php 2 weeks ago Archive.php 2 months ago ArchiveProcessor.php 2 weeks ago AssetManager.php 2 weeks ago Auth.php 8 months ago AuthResult.php 8 months ago BaseFactory.php 2 years ago Cache.php 2 weeks ago CacheId.php 5 months ago CliMulti.php 2 weeks ago Common.php 2 weeks ago Config.php 2 weeks ago Console.php 4 months ago Context.php 2 years ago Cookie.php 2 weeks ago CronArchive.php 2 weeks ago DI.php 4 months ago DataArray.php 2 weeks ago DataTable.php 2 weeks ago Date.php 2 months ago Db.php 2 months ago DbHelper.php 2 weeks ago Development.php 2 weeks ago ErrorHandler.php 8 months ago EventDispatcher.php 2 months ago ExceptionHandler.php 5 months ago FileIntegrity.php 2 weeks ago Filechecks.php 1 year ago Filesystem.php 2 weeks ago FrontController.php 2 weeks ago Http.php 2 weeks ago IP.php 1 year ago Log.php 4 months ago LogDeleter.php 2 weeks ago Mail.php 1 year ago Metrics.php 2 months ago NoAccessException.php 2 years ago Nonce.php 8 months ago Notification.php 2 months ago NumberFormatter.php 2 weeks ago Option.php 2 weeks ago Period.php 2 weeks ago Piwik.php 2 weeks ago Plugin.php 2 months ago Process.php 2 months ago Profiler.php 2 weeks ago ProxyHeaders.php 5 months ago ProxyHttp.php 2 weeks ago QuickForm2.php 4 months ago RankingQuery.php 2 months ago ReportRenderer.php 2 weeks ago Request.php 2 months ago Segment.php 2 weeks ago Sequence.php 2 weeks ago Session.php 1 month ago SettingsPiwik.php 2 weeks ago SettingsServer.php 2 weeks ago Singleton.php 2 years ago Site.php 2 months ago SiteContentDetector.php 2 months ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 2 weeks ago Timer.php 2 weeks ago Tracker.php 2 weeks ago Twig.php 2 months ago Unzip.php 1 year ago UpdateCheck.php 2 months ago Updater.php 2 weeks ago UpdaterErrorException.php 2 years ago Updates.php 2 weeks ago Url.php 2 weeks ago UrlHelper.php 2 months ago Version.php 2 weeks ago View.php 2 weeks ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 8 months ago
ProxyHttp.php
256 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik;
10
11 /**
12 * Http helper: static file server proxy, with compression, caching, isHttps() helper...
13 *
14 * Used to server piwik.js and the merged+minified CSS and JS files
15 *
16 */
17 class ProxyHttp
18 {
19 public const DEFLATE_ENCODING_REGEX = '/(?:^|, ?)(deflate)(?:,|$)/';
20 public const GZIP_ENCODING_REGEX = '/(?:^|, ?)((x-)?gzip)(?:,|$)/';
21 /**
22 * Returns true if the current request appears to be a secure HTTPS connection
23 *
24 * @return bool
25 */
26 public static function isHttps()
27 {
28 return \Piwik\Url::getCurrentScheme() === 'https';
29 }
30 /**
31 * Serve static files through php proxy.
32 *
33 * It performs the following actions:
34 * - Checks the file is readable or returns "HTTP/1.0 404 Not Found"
35 * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE
36 * with the modification date of the static file
37 * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in
38 * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file
39 * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension.
40 * Using manually compressed static files requires you to manually update the compressed file when
41 * the static file is updated.
42 * - Overrides server cache control config to allow caching
43 * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according
44 * to users encoding capacities.
45 *
46 * Warning:
47 * Compressed filed are stored in the /tmp directory.
48 * If this method is used with two files bearing the same name but located in different locations,
49 * there is a risk of conflict. One file could be served with the content of the other.
50 * A future upgrade of this method would be to recreate the directory structure of the static file
51 * within a /tmp/compressed-static-files directory.
52 *
53 * @param string $file The location of the static file to serve
54 * @param string $contentType The content type of the static file.
55 * @param int|false $expireFarFutureDays Day in the far future to set the Expires header to.
56 * Should be set to false for files that should not be cached.
57 * @param int|false $byteStart The starting byte in the file to serve. If false, the data from the beginning
58 * of the file will be served.
59 * @param int|false $byteEnd The ending byte in the file to serve. If false, the data from $byteStart to the
60 * end of the file will be served.
61 * @param string|false $filename By default the filename of $file is reused as Content-Disposition. If the
62 * file should be sent as a different filename to the client you can specify
63 * a custom filename here.
64 */
65 public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100, $byteStart = \false, $byteEnd = \false, $filename = \false)
66 {
67 // if the file cannot be found return HTTP status code '404'
68 if (empty($file) || !file_exists($file)) {
69 \Piwik\Common::sendResponseCode(404);
70 return;
71 }
72 if (!is_readable($file)) {
73 \Piwik\Common::sendResponseCode(500);
74 return;
75 }
76 $modifiedSince = \Piwik\Http::getModifiedSinceHeader();
77 $fileModifiedTime = @filemtime($file);
78 $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT';
79 // set some HTTP response headers
80 self::overrideCacheControlHeaders('public');
81 \Piwik\Common::sendHeader('Vary: Accept-Encoding');
82 if (\false === $filename) {
83 $filename = basename($file);
84 }
85 \Piwik\Common::sendHeader('Content-Disposition: inline; filename=' . $filename);
86 if ($expireFarFutureDays) {
87 // Required by proxy caches potentially in between the browser and server to cache the request indeed
88 \Piwik\Common::sendHeader(self::getExpiresHeaderForFutureDay($expireFarFutureDays));
89 }
90 // Return 304 if the file has not modified since
91 if ($modifiedSince === $lastModified) {
92 \Piwik\Common::sendResponseCode(304);
93 return;
94 }
95 // if we have to serve the file, serve it now, either in the clear or compressed
96 if ($byteStart === \false) {
97 $byteStart = 0;
98 }
99 if ($byteEnd === \false) {
100 $byteEnd = filesize($file);
101 }
102 $compressed = \false;
103 $encoding = '';
104 $compressedFileLocation = \Piwik\AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file);
105 if (!($byteStart == 0 && $byteEnd == filesize($file))) {
106 $compressedFileLocation .= ".{$byteStart}.{$byteEnd}";
107 }
108 $phpOutputCompressionEnabled = self::isPhpOutputCompressed();
109 if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) {
110 [$encoding, $extension] = self::getCompressionEncodingAcceptedByClient();
111 $filegz = $compressedFileLocation . $extension;
112 if (self::canCompressInPhp()) {
113 if (!empty($encoding)) {
114 // compress the file if it doesn't exist or is newer than the existing cached file, and cache
115 // the compressed result
116 if (self::shouldCompressFile($file, $filegz)) {
117 self::compressFile($file, $filegz, $encoding, $byteStart, $byteEnd);
118 }
119 $compressed = \true;
120 $file = $filegz;
121 $byteStart = 0;
122 $byteEnd = filesize($file);
123 }
124 } else {
125 // if a compressed file exists, the file was manually compressed so we just serve that
126 if ($extension == '.gz' && !self::shouldCompressFile($file, $filegz)) {
127 $compressed = \true;
128 $file = $filegz;
129 $byteStart = 0;
130 $byteEnd = filesize($file);
131 }
132 }
133 }
134 \Piwik\Common::sendHeader('Last-Modified: ' . $lastModified);
135 if (!$phpOutputCompressionEnabled) {
136 \Piwik\Common::sendHeader('Content-Length: ' . ($byteEnd - $byteStart));
137 }
138 if (!empty($contentType)) {
139 \Piwik\Common::sendHeader('Content-Type: ' . $contentType);
140 }
141 if ($compressed) {
142 \Piwik\Common::sendHeader('Content-Encoding: ' . $encoding);
143 }
144 // in case any notices were triggered before this point (eg in WordPress) etc.
145 // it would break the gzipped response since it would have mixed regular notice/string plus gzipped content
146 // and would not be able to decode the response
147 $levels = ob_get_level();
148 for ($i = 0; $i < $levels; $i++) {
149 ob_end_clean();
150 }
151 // clearing all output buffers combined with output compressions had bugs on certain PHP versions
152 // manually removing the Content-Encoding header fixes this
153 // See https://github.com/php/php-src/issues/8218
154 if ($phpOutputCompressionEnabled && (version_compare(\PHP_VERSION, '8.0.17', '=') || version_compare(\PHP_VERSION, '8.0.18', '=') || version_compare(\PHP_VERSION, '8.1.4', '=') || version_compare(\PHP_VERSION, '8.1.5', '='))) {
155 header_remove("Content-Encoding");
156 }
157 if (!_readfile($file, $byteStart, $byteEnd)) {
158 \Piwik\Common::sendResponseCode(500);
159 }
160 }
161 /**
162 * Test if php output is compressed
163 *
164 * @return bool True if php output is (or suspected/likely) to be compressed
165 */
166 public static function isPhpOutputCompressed()
167 {
168 // Off = ''; On = '1'; otherwise, it's a buffer size
169 $zlibOutputCompression = ini_get('zlib.output_compression');
170 // could be ob_gzhandler, ob_deflatehandler, etc
171 $outputHandler = ini_get('output_handler');
172 // output handlers can be stacked
173 $obHandlers = array_filter(ob_list_handlers(), function ($var) {
174 return $var !== "default output handler";
175 });
176 // user defined handler via wrapper
177 if (!defined('PIWIK_TEST_MODE')) {
178 $autoPrependFile = ini_get('auto_prepend_file');
179 $autoAppendFile = ini_get('auto_append_file');
180 }
181 return !empty($zlibOutputCompression) || !empty($outputHandler) || !empty($obHandlers) || !empty($autoPrependFile) || !empty($autoAppendFile);
182 }
183 /**
184 * Workaround IE bug when downloading certain document types over SSL and
185 * cache control headers are present, e.g.,
186 *
187 * Cache-Control: no-cache
188 * Cache-Control: no-store,max-age=0,must-revalidate
189 * Pragma: no-cache
190 *
191 * @see https://support.microsoft.com/kb/316431/
192 * @see RFC2616
193 *
194 * @param string|null $override One of "public", "private", "no-cache", or "no-store". (optional)
195 */
196 public static function overrideCacheControlHeaders($override = null)
197 {
198 if ($override || self::isHttps()) {
199 \Piwik\Common::stripHeader('Pragma');
200 \Piwik\Common::stripHeader('Expires');
201 if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) {
202 \Piwik\Common::sendHeader("Cache-Control: {$override}, must-revalidate");
203 } else {
204 \Piwik\Common::sendHeader('Cache-Control: must-revalidate');
205 }
206 }
207 }
208 /**
209 * Returns a formatted Expires HTTP header for a certain number of days in the future. The result
210 * can be used in a call to `header()`.
211 */
212 private static function getExpiresHeaderForFutureDay($expireFarFutureDays)
213 {
214 return "Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * (int) $expireFarFutureDays) . ' GMT';
215 }
216 private static function getCompressionEncodingAcceptedByClient()
217 {
218 $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
219 if (preg_match(self::GZIP_ENCODING_REGEX, $acceptEncoding, $matches)) {
220 return array('gzip', '.gz');
221 } elseif (preg_match(self::DEFLATE_ENCODING_REGEX, $acceptEncoding, $matches)) {
222 return array('deflate', '.deflate');
223 } else {
224 return array(\false, \false);
225 }
226 }
227 private static function canCompressInPhp()
228 {
229 return extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents');
230 }
231 private static function shouldCompressFile($fileToCompress, $compressedFilePath)
232 {
233 $toCompressLastModified = @filemtime($fileToCompress);
234 $compressedLastModified = @filemtime($compressedFilePath);
235 return !file_exists($compressedFilePath) || $toCompressLastModified > $compressedLastModified;
236 }
237 private static function compressFile($fileToCompress, $compressedFilePath, $compressionEncoding, $byteStart, $byteEnd)
238 {
239 $data = file_get_contents($fileToCompress);
240 $data = substr($data, $byteStart, $byteEnd - $byteStart);
241 if ($compressionEncoding == 'deflate') {
242 $data = gzdeflate($data, 9);
243 } elseif ($compressionEncoding == 'gzip' || $compressionEncoding == 'x-gzip') {
244 $data = self::gzencode($data);
245 }
246 if (\false === $data) {
247 throw new \Exception('compressing file ' . $fileToCompress . ' failed');
248 }
249 file_put_contents($compressedFilePath, $data);
250 }
251 public static function gzencode($data)
252 {
253 return gzencode($data, 9);
254 }
255 }
256