PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
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 / Common.php
matomo / app / core Last commit date
API 3 months ago Access 3 months ago Application 3 months ago Archive 3 months ago ArchiveProcessor 3 months ago Archiver 2 years ago AssetManager 3 months ago Auth 6 months ago Category 6 months ago Changes 3 months ago CliMulti 1 year ago Columns 3 months ago Concurrency 3 months ago Config 3 months ago Container 3 months ago CronArchive 3 months ago DataAccess 3 months ago DataFiles 2 years ago DataTable 3 months ago Db 3 months ago DeviceDetector 1 year ago Email 2 years ago Exception 4 months ago Http 4 months ago Intl 3 months ago Log 2 years ago Mail 1 year ago Measurable 6 months ago Menu 3 months ago Metrics 3 months ago Notification 6 months ago Period 3 months ago Plugin 3 months ago Policy 3 months ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 3 months ago Request 3 months ago Scheduler 3 months ago Segment 3 months ago Session 3 months ago Settings 3 months ago Tracker 3 months ago Translation 3 months ago Twig 1 year ago UpdateCheck 3 months ago Updater 4 months ago Updates 3 months ago Validators 1 year ago View 6 months ago ViewDataTable 3 months ago Visualization 1 year ago Widget 3 months ago .htaccess 2 years ago Access.php 3 months ago Archive.php 3 months ago ArchiveProcessor.php 4 months ago AssetManager.php 3 months ago Auth.php 6 months ago AuthResult.php 6 months ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 4 months ago CliMulti.php 3 months ago Common.php 3 months ago Config.php 3 months ago Console.php 3 months ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 3 months ago DI.php 3 months ago DataArray.php 5 months ago DataTable.php 3 months ago Date.php 3 months ago Db.php 3 months ago DbHelper.php 3 months ago Development.php 1 year ago ErrorHandler.php 6 months ago EventDispatcher.php 1 year ago ExceptionHandler.php 4 months ago FileIntegrity.php 3 months ago Filechecks.php 1 year ago Filesystem.php 3 months ago FrontController.php 4 months ago Http.php 4 months ago IP.php 1 year ago Log.php 3 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 3 months ago NoAccessException.php 2 years ago Nonce.php 6 months ago Notification.php 6 months ago NumberFormatter.php 5 months ago Option.php 5 months ago Period.php 3 months ago Piwik.php 3 months ago Plugin.php 3 months ago Process.php 1 year ago Profiler.php 6 months ago ProxyHeaders.php 4 months ago ProxyHttp.php 5 months ago QuickForm2.php 3 months ago RankingQuery.php 5 months ago ReportRenderer.php 3 months ago Request.php 3 months ago Segment.php 3 months ago Sequence.php 6 months ago Session.php 3 months ago SettingsPiwik.php 3 months ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 4 months ago SiteContentDetector.php 3 months ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 3 months ago Twig.php 3 months ago Unzip.php 1 year ago UpdateCheck.php 3 months ago Updater.php 3 months ago UpdaterErrorException.php 2 years ago Updates.php 3 months ago Url.php 3 months ago UrlHelper.php 3 months ago Version.php 3 months ago View.php 3 months ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 6 months ago
Common.php
1105 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 use Exception;
12 use Piwik\CliMulti\Process;
13 use Piwik\Container\StaticContainer;
14 use Piwik\Intl\Data\Provider\LanguageDataProvider;
15 use Piwik\Intl\Data\Provider\RegionDataProvider;
16 use Piwik\Log\LoggerInterface;
17 use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
18 use Piwik\Plugins\PrivacyManager\FeatureFlags\PrivacyCompliance;
19 use Piwik\Plugins\PrivacyManager\Settings\CampaignTrackingParametersDisabled;
20 use Piwik\Tracker\Cache as TrackerCache;
21 /**
22 * Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
23 *
24 * This is the only non-Tracker class loaded by the **\/piwik.php** file.
25 */
26 class Common
27 {
28 // constants used to map the referrer type to an integer in the log_visit table
29 public const REFERRER_TYPE_DIRECT_ENTRY = 1;
30 public const REFERRER_TYPE_SEARCH_ENGINE = 2;
31 public const REFERRER_TYPE_WEBSITE = 3;
32 public const REFERRER_TYPE_CAMPAIGN = 6;
33 public const REFERRER_TYPE_SOCIAL_NETWORK = 7;
34 public const REFERRER_TYPE_AI_ASSISTANT = 8;
35 // Flag used with htmlspecialchar. See php.net/htmlspecialchars.
36 public const HTML_ENCODING_QUOTE_STYLE = \ENT_QUOTES;
37 public static $isCliMode = null;
38 /**
39 * Filled and used during tests only
40 * @var array
41 */
42 public static $headersSentInTests = [];
43 /*
44 * Database
45 */
46 public const LANGUAGE_CODE_INVALID = 'xx';
47 /**
48 * Hashes a string into an integer which should be very low collision risks
49 * @param string $string String to hash
50 * @return int Resulting int hash
51 */
52 public static function hashStringToInt($string)
53 {
54 $stringHash = substr(md5($string), 0, 8);
55 return base_convert($stringHash, 16, 10);
56 }
57 /**
58 * Returns a prefixed table name.
59 *
60 * The table prefix is determined by the `[database] tables_prefix` INI config
61 * option.
62 *
63 * @param string $table The table name to prefix, ie "log_visit"
64 * @return string The prefixed name, ie "piwik-production_log_visit".
65 * @api
66 */
67 public static function prefixTable($table)
68 {
69 $prefix = \Piwik\Config::getInstance()->database['tables_prefix'];
70 return $prefix . $table;
71 }
72 /**
73 * Returns an array containing the prefixed table names of every passed argument.
74 *
75 * @param string ...$tables The table names to prefix, ie "log_visit"
76 * @return array The prefixed names in an array.
77 */
78 public static function prefixTables(...$tables)
79 {
80 $result = array();
81 foreach ($tables as $table) {
82 $result[] = self::prefixTable($table);
83 }
84 return $result;
85 }
86 /**
87 * Removes the prefix from a table name and returns the result.
88 *
89 * The table prefix is determined by the `[database] tables_prefix` INI config
90 * option.
91 *
92 * @param string $table The prefixed table name, eg "piwik-production_log_visit".
93 * @return string The unprefixed table name, eg "log_visit".
94 * @api
95 */
96 public static function unprefixTable($table)
97 {
98 $prefixTable = \Piwik\Config::getInstance()->database['tables_prefix'];
99 if (empty($prefixTable) || strpos($table, $prefixTable) !== 0) {
100 return $table;
101 }
102 return substr($table, strlen($prefixTable));
103 }
104 /*
105 * Tracker
106 */
107 public static function isGoalPluginEnabled()
108 {
109 return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('Goals');
110 }
111 public static function isActionsPluginEnabled()
112 {
113 return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('Actions');
114 }
115 /**
116 * Returns true if PHP was invoked from command-line interface (shell)
117 *
118 * @since added in 0.4.4
119 * @return bool true if PHP invoked as a CGI or from CLI
120 */
121 public static function isPhpCliMode()
122 {
123 if (is_bool(self::$isCliMode)) {
124 return self::$isCliMode;
125 }
126 if (\PHP_SAPI === 'cli') {
127 return \true;
128 }
129 if (self::isPhpCgiType() && (!isset($_SERVER['REMOTE_ADDR']) || empty($_SERVER['REMOTE_ADDR']))) {
130 return \true;
131 }
132 return \false;
133 }
134 /**
135 * Returns true if PHP is executed as CGI type.
136 *
137 * @since added in 0.4.4
138 * @return bool true if PHP invoked as a CGI
139 */
140 public static function isPhpCgiType()
141 {
142 $sapiType = php_sapi_name();
143 return substr($sapiType, 0, 3) === 'cgi';
144 }
145 /**
146 * Returns true if the current request is a console command, eg.
147 * ./console xx:yy
148 * or
149 * php console xx:yy
150 *
151 * @return bool
152 */
153 public static function isRunningConsoleCommand()
154 {
155 $searched = 'console';
156 $consolePos = strpos($_SERVER['SCRIPT_NAME'], $searched);
157 $expectedConsolePos = strlen($_SERVER['SCRIPT_NAME']) - strlen($searched);
158 $isScriptIsConsole = $consolePos === $expectedConsolePos;
159 return self::isPhpCliMode() && $isScriptIsConsole;
160 }
161 /*
162 * String operations
163 */
164 /**
165 * Multi-byte substr() - works with UTF-8.
166 *
167 * Calls `mb_substr` if available and falls back to `substr` if it's not.
168 *
169 * @param string $string
170 * @param int $start
171 * @param int|null $length optional length
172 * @return string
173 * @deprecated since 4.4 - directly use mb_substr instead
174 */
175 public static function mb_substr($string, $start, $length = null)
176 {
177 return mb_substr($string, $start, $length, 'UTF-8');
178 }
179 /**
180 * Gets the current process ID.
181 * Note: If getmypid is disabled, a random ID will be generated once and used throughout the request. There is a
182 * small chance that two processes at the same time may generated the same random ID. If you need to rely on the
183 * value being 100% unique, then you may need to use `getmypid` directly or some other logic. Eg in CliMulti it is
184 * fine to use `getmypid` directly as the logic won't be used if getmypid is disabled...
185 * If you are wanting to use the pid to check if the process is running eg using `ps`, then you also have to use
186 * getmypid directly.
187 *
188 * @return int|null
189 */
190 public static function getProcessId()
191 {
192 static $pid;
193 if (!isset($pid)) {
194 if (Process::isMethodDisabled('getmypid')) {
195 $pid = \Piwik\Common::getRandomInt(12);
196 } else {
197 $pid = \getmypid();
198 }
199 }
200 return $pid;
201 }
202 /**
203 * Multi-byte strlen() - works with UTF-8
204 *
205 * Calls `mb_substr` if available and falls back to `substr` if not.
206 *
207 * @param string $string
208 * @return int
209 * @deprecated since 4.4 - directly use mb_strlen instead
210 */
211 public static function mb_strlen($string)
212 {
213 return mb_strlen($string, 'UTF-8');
214 }
215 /**
216 * Multi-byte strtolower() - works with UTF-8.
217 *
218 * Calls `mb_strtolower` if available and falls back to `strtolower` if not.
219 *
220 * @param string $string
221 * @return string
222 * @deprecated since 4.4 - directly use mb_strtolower instead
223 */
224 public static function mb_strtolower($string)
225 {
226 return mb_strtolower($string, 'UTF-8');
227 }
228 /**
229 * Multi-byte strtoupper() - works with UTF-8.
230 *
231 * Calls `mb_strtoupper` if available and falls back to `strtoupper` if not.
232 *
233 * @param string $string
234 * @return string
235 * @deprecated since 4.4 - directly use mb_strtoupper instead
236 */
237 public static function mb_strtoupper($string)
238 {
239 return mb_strtoupper($string, 'UTF-8');
240 }
241 /**
242 * Timing attack safe string comparison.
243 *
244 * @return bool
245 */
246 public static function hashEquals(string $stringA, string $stringB)
247 {
248 if (function_exists('hash_equals')) {
249 return hash_equals($stringA, $stringB);
250 }
251 if (strlen($stringA) !== strlen($stringB)) {
252 return \false;
253 }
254 $result = "\x00";
255 $stringA ^= $stringB;
256 for ($i = 0; $i < strlen($stringA); $i++) {
257 $result |= $stringA[$i];
258 }
259 return $result === "\x00";
260 }
261 /**
262 * Secure wrapper for unserialize, which by default disallows unserializing classes
263 *
264 * @param string|null $string String to unserialize
265 * @param array $allowedClasses Class names that should be allowed to unserialize
266 * @param bool $rethrow Whether to rethrow exceptions or not.
267 * @return mixed
268 */
269 public static function safe_unserialize($string, $allowedClasses = [], $rethrow = \false)
270 {
271 try {
272 // phpcs:ignore Generic.PHP.ForbiddenFunctions
273 return unserialize($string ?? '', ['allowed_classes' => empty($allowedClasses) ? \false : $allowedClasses]);
274 } catch (\Throwable $e) {
275 if ($rethrow) {
276 throw $e;
277 }
278 $logger = StaticContainer::get(LoggerInterface::class);
279 $logger->debug('Unable to unserialize a string: {exception} (string = {string})', ['exception' => $e, 'string' => $string]);
280 return \false;
281 }
282 }
283 /*
284 * Escaping input
285 */
286 /**
287 * Sanitizes a string to help avoid XSS vulnerabilities.
288 *
289 * This function is automatically called when {@link getRequestVar()} is called,
290 * so you should not normally have to use it.
291 *
292 * This function should be used when outputting data that isn't escaped and was
293 * obtained from the user (for example when using the `|raw` twig filter on goal names).
294 *
295 * _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders
296 * should still be used._
297 *
298 * **Implementation Details**
299 *
300 * - [htmlspecialchars](https://php.net/manual/en/function.htmlspecialchars.php) is used to escape text.
301 * - Single quotes are not escaped so **Piwik's amazing community** will still be
302 * **Piwik's amazing community**.
303 * - Use of the `magic_quotes` setting will not break this method.
304 * - Boolean, numeric and null values are not modified.
305 *
306 * @param mixed $value The variable to be sanitized. If an array is supplied, the contents
307 * of the array will be sanitized recursively. The keys of the array
308 * will also be sanitized.
309 * @param bool $alreadyStripslashed Implementation detail, ignore.
310 * @throws Exception If `$value` is of an incorrect type.
311 * @return mixed The sanitized value.
312 * @api
313 */
314 public static function sanitizeInputValues($value, $alreadyStripslashed = \false)
315 {
316 if (is_numeric($value)) {
317 return $value;
318 } elseif (is_string($value)) {
319 $value = self::sanitizeString($value);
320 } elseif (is_array($value)) {
321 foreach (array_keys($value) as $key) {
322 $newKey = $key;
323 $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
324 if ($key !== $newKey) {
325 $value[$newKey] = $value[$key];
326 unset($value[$key]);
327 }
328 $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
329 }
330 } elseif (!is_null($value) && !is_bool($value)) {
331 throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, \true));
332 }
333 return $value;
334 }
335 /**
336 * Sanitize a single input value and removes line breaks, tabs and null characters.
337 *
338 * @param string $value
339 * @return string sanitized input
340 */
341 public static function sanitizeInputValue($value)
342 {
343 $value = self::sanitizeLineBreaks($value);
344 $value = self::sanitizeString($value);
345 return $value;
346 }
347 /**
348 * Sanitize a single input value
349 *
350 * @param $value
351 * @return string
352 */
353 private static function sanitizeString($value)
354 {
355 // $_GET and $_REQUEST already urldecode()'d
356 // decode
357 // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
358 $value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
359 $value = self::sanitizeNullBytes($value);
360 // escape
361 $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
362 // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
363 if ($value !== '' && $tmp === '') {
364 // convert and escape
365 $value = utf8_encode($value);
366 $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
367 return $tmp;
368 }
369 return $tmp;
370 }
371 /**
372 * Unsanitizes a single input value and returns the result.
373 *
374 * @param string|null $value
375 * @return string unsanitized input
376 * @api
377 */
378 public static function unsanitizeInputValue($value)
379 {
380 return htmlspecialchars_decode($value ?? '', self::HTML_ENCODING_QUOTE_STYLE);
381 }
382 /**
383 * Unsanitizes one or more values and returns the result.
384 *
385 * This method should be used when you need to unescape data that was obtained from
386 * the user.
387 *
388 * Some data in Piwik is stored sanitized (such as site name). In this case you may
389 * have to use this method to unsanitize it in order to, for example, output it in JSON.
390 *
391 * @param string|array $value The data to unsanitize. If an array is passed, the
392 * array is sanitized recursively. Key values are not unsanitized.
393 * @return string|array The unsanitized data.
394 * @api
395 */
396 public static function unsanitizeInputValues($value)
397 {
398 if (is_array($value)) {
399 $result = array();
400 foreach ($value as $key => $arrayValue) {
401 $result[$key] = self::unsanitizeInputValues($arrayValue);
402 }
403 return $result;
404 } else {
405 return self::unsanitizeInputValue($value);
406 }
407 }
408 /**
409 * @param string $value
410 * @return string Line breaks and line carriage removed
411 */
412 public static function sanitizeLineBreaks($value)
413 {
414 return is_null($value) ? '' : str_replace(array("\n", "\r"), '', $value);
415 }
416 /**
417 * @param string $value
418 * @return string Null bytes removed
419 */
420 public static function sanitizeNullBytes($value)
421 {
422 return str_replace(array("\x00"), '', $value);
423 }
424 /**
425 * Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals.
426 *
427 * Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._**
428 *
429 * If the variable cannot be found, and a default value was not provided, an exception is raised.
430 *
431 * _See {@link sanitizeInputValues()} to learn more about sanitization._
432 *
433 * @param string $varName Name of the request parameter to get. By default, we look in `$_GET[$varName]`
434 * and `$_POST[$varName]` for the value.
435 * @param mixed $varDefault The value to return if the request parameter cannot be found or has an empty value.
436 * @param string|null $varType Expected type of the request variable. This parameters value must be one of the following:
437 * `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`.
438 *
439 * If `'json'`, the string value will be `json_decode`-d and then sanitized.
440 * @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`.
441 * @return mixed The sanitized request parameter.
442 * @phpstan-return ($varType is 'array' ? array : ($varType is 'integer' ? int : ($varType is 'int' ? int : ($varType is 'float' ? float : ($varType is 'string' ? string : ($varType is 'json' ? array|bool|float|int|string|null : mixed))))))
443 * @throws Exception If the request parameter doesn't exist and there is no default value, or if the request parameter
444 * exists but has an incorrect type.
445 * @see Request::getParameter()
446 * @deprecated Use Request class instead, which will return raw values instead.
447 * @api
448 */
449 public static function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null)
450 {
451 if (is_null($requestArrayToUse)) {
452 $requestArrayToUse = $_GET + $_POST;
453 }
454 $varDefault = self::sanitizeInputValues($varDefault);
455 if ($varType === 'int') {
456 // settype accepts only integer
457 // 'int' is simply a shortcut for 'integer'
458 $varType = 'integer';
459 }
460 // there is no value $varName in the REQUEST so we try to use the default value
461 if (empty($varName) || !isset($requestArrayToUse[$varName]) || !is_array($requestArrayToUse[$varName]) && strlen($requestArrayToUse[$varName]) === 0) {
462 if (is_null($varDefault)) {
463 throw new Exception("The parameter '{$varName}' isn't set in the Request, and a default value wasn't provided.");
464 } else {
465 if (!is_null($varType) && in_array($varType, array('string', 'integer', 'array'))) {
466 settype($varDefault, $varType);
467 }
468 return $varDefault;
469 }
470 }
471 // Normal case, there is a value available in REQUEST for the requested varName:
472 // we deal w/ json differently
473 if ($varType === 'json') {
474 $value = $requestArrayToUse[$varName];
475 if (is_string($value)) {
476 $value = json_decode($value, $assoc = \true);
477 }
478 return self::sanitizeInputValues($value, \true);
479 }
480 $value = self::sanitizeInputValues($requestArrayToUse[$varName]);
481 if (isset($varType)) {
482 $ok = \false;
483 if ($varType === 'string') {
484 if (is_string($value) || is_int($value)) {
485 $ok = \true;
486 } elseif (is_float($value)) {
487 $value = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($value);
488 $ok = \true;
489 }
490 } elseif ($varType === 'integer') {
491 if ($value == (string) (int) $value) {
492 $ok = \true;
493 }
494 } elseif ($varType === 'float') {
495 $valueToCompare = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($value);
496 // Simplified regex for float without support for underscore notation
497 // will match: 1.234, 1.2e3, 7E-10
498 // won't match: 1_234.567
499 $floatRegex = "/^[+-]?((([0-9]+)|(([0-9]+)?\\.([0-9]+))|(([0-9]+)\\.([0-9]+)?))([eE][+-]?([0-9]+))?)\$/";
500 if (preg_match($floatRegex, $valueToCompare)) {
501 $ok = \true;
502 }
503 } elseif ($varType === 'array') {
504 if (is_array($value)) {
505 $ok = \true;
506 }
507 } else {
508 throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
509 }
510 // The type is not correct
511 if ($ok === \false) {
512 if ($varDefault === null) {
513 throw new Exception("The parameter '{$varName}' doesn't have a correct type, and a default value wasn't provided.");
514 } else {
515 // we return the default value with the good type set
516 settype($varDefault, $varType);
517 return $varDefault;
518 }
519 }
520 settype($value, $varType);
521 }
522 return $value;
523 }
524 /*
525 * Generating unique strings
526 */
527 /**
528 * Generates a random integer
529 *
530 * @param int $min
531 * @param null|int $max Defaults to max int value
532 * @return int
533 */
534 public static function getRandomInt($min = 0, $max = null)
535 {
536 if (!isset($max)) {
537 $max = \PHP_INT_MAX;
538 }
539 return random_int($min, $max);
540 }
541 /**
542 * Returns a 32 characters long uniq ID
543 *
544 * @return string 32 chars
545 */
546 public static function generateUniqId()
547 {
548 return bin2hex(random_bytes(16));
549 }
550 /**
551 * Configurable hash() algorithm (defaults to md5)
552 *
553 * @param string $str String to be hashed
554 * @param bool $raw_output
555 * @return string Hash string
556 */
557 public static function hash($str, $raw_output = \false)
558 {
559 static $hashAlgorithm = null;
560 if (is_null($hashAlgorithm)) {
561 $hashAlgorithm = @\Piwik\Config::getInstance()->General['hash_algorithm'];
562 }
563 if ($hashAlgorithm) {
564 $hash = @hash($hashAlgorithm, $str, $raw_output);
565 if ($hash !== \false) {
566 return $hash;
567 }
568 }
569 return md5($str, $raw_output);
570 }
571 /**
572 * Generate random string.
573 *
574 * @param int $length string length
575 * @param string $alphabet characters allowed in random string
576 * @return string random string with given length
577 */
578 public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
579 {
580 $chars = $alphabet;
581 $str = '';
582 for ($i = 0; $i < $length; $i++) {
583 $rand_key = self::getRandomInt(0, strlen($chars) - 1);
584 $str .= substr($chars, $rand_key, 1);
585 }
586 return str_shuffle($str);
587 }
588 /*
589 * Conversions
590 */
591 /**
592 * Convert hexadecimal representation into binary data.
593 * !! Will emit warning if input string is not hex!!
594 *
595 * @see https://php.net/bin2hex
596 *
597 * @param string $str Hexadecimal representation
598 * @return string
599 */
600 public static function hex2bin($str)
601 {
602 return pack("H*", $str);
603 }
604 /**
605 * This function will convert the input string to the binary representation of the ID
606 * but it will throw an Exception if the specified input ID is not correct
607 *
608 * This is used when building segments containing visitorId which could be an invalid string
609 * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
610 *
611 * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
612 * so better be safe and get the php error when something unexpected is happening
613 * @param string $id
614 * @throws Exception
615 * @return string binary string
616 */
617 public static function convertVisitorIdToBin($id)
618 {
619 if (strlen($id) !== \Piwik\Tracker::LENGTH_HEX_ID_STRING || @bin2hex(self::hex2bin($id)) != $id) {
620 throw new Exception("visitorId is expected to be a " . \Piwik\Tracker::LENGTH_HEX_ID_STRING . " hex char string");
621 }
622 return self::hex2bin($id);
623 }
624 /**
625 * Converts a User ID string to the Visitor ID Binary representation.
626 *
627 * @param $userId
628 * @return string
629 */
630 public static function convertUserIdToVisitorIdBin($userId)
631 {
632 $userIdHashed = \MatomoTracker::getUserIdHashed($userId);
633 return self::convertVisitorIdToBin($userIdHashed);
634 }
635 /**
636 * Detects whether an error occurred during the last json encode/decode.
637 * @return bool
638 */
639 public static function hasJsonErrorOccurred()
640 {
641 return json_last_error() != \JSON_ERROR_NONE;
642 }
643 /**
644 * Returns a human readable error message in case an error occurred during the last json encode/decode.
645 * Returns an empty string in case there was no error.
646 *
647 * @return string
648 */
649 public static function getLastJsonError()
650 {
651 switch (json_last_error()) {
652 case \JSON_ERROR_NONE:
653 return '';
654 case \JSON_ERROR_DEPTH:
655 return 'Maximum stack depth exceeded';
656 case \JSON_ERROR_STATE_MISMATCH:
657 return 'Underflow or the modes mismatch';
658 case \JSON_ERROR_CTRL_CHAR:
659 return 'Unexpected control character found';
660 case \JSON_ERROR_SYNTAX:
661 return 'Syntax error, malformed JSON';
662 case \JSON_ERROR_UTF8:
663 return 'Malformed UTF-8 characters, possibly incorrectly encoded';
664 }
665 return 'Unknown error';
666 }
667 public static function stringEndsWith($haystack, $needle)
668 {
669 if (strlen(strval($needle)) === 0) {
670 return \true;
671 }
672 if (strlen(strval($haystack)) === 0) {
673 return \false;
674 }
675 $lastCharacters = substr($haystack, -strlen($needle));
676 return $lastCharacters === $needle;
677 }
678 /**
679 * Returns the list of parent classes for the given class.
680 *
681 * @param string $class A class name.
682 * @return string[] The list of parent classes in order from highest ancestor to the descended class.
683 */
684 public static function getClassLineage($class)
685 {
686 $classes = array_merge(array($class), array_values(class_parents($class, $autoload = \false)));
687 return array_reverse($classes);
688 }
689 /*
690 * DataFiles
691 */
692 /**
693 * Returns list of provider names
694 *
695 * @see core/DataFiles/Providers.php
696 *
697 * @return array Array of ( dnsName => providerName )
698 */
699 public static function getProviderNames()
700 {
701 require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
702 $providers = $GLOBALS['Piwik_ProviderNames'];
703 return $providers;
704 }
705 /*
706 * Language, country, continent
707 */
708 /**
709 * Returns the browser language code, eg. "en-gb,en;q=0.5"
710 *
711 * @param string|null $browserLang Optional browser language, otherwise taken from the request header
712 * @return string
713 */
714 public static function getBrowserLanguage($browserLang = null)
715 {
716 static $replacementPatterns = array(
717 // extraneous bits of RFC 3282 that we ignore
718 '/(\\\\.)/',
719 // quoted-pairs
720 '/(\\s+)/',
721 // CFWcS white space
722 '/(\\([^)]*\\))/',
723 // CFWS comments
724 '/(;q=[0-9.]+)/',
725 // quality
726 // found in the LANG environment variable
727 '/\\.(.*)/',
728 // charset (e.g., en_CA.UTF-8)
729 '/^C$/',
730 );
731 if (is_null($browserLang)) {
732 $browserLang = self::sanitizeInputValues($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
733 if (empty($browserLang) && self::isPhpCliMode()) {
734 $browserLang = @getenv('LANG');
735 }
736 }
737 if (empty($browserLang)) {
738 // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
739 $browserLang = "";
740 } else {
741 // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
742 // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
743 // but we guard against a bad user agent which naively uses its locale
744 $browserLang = strtolower(str_replace('_', '-', $browserLang));
745 // filters
746 $browserLang = preg_replace($replacementPatterns, '', $browserLang);
747 $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1);
748 // Firefox bug
749 $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1);
750 // unregistered language tag
751 $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang);
752 // unofficial (proposed) code in the wild
753 }
754 return $browserLang;
755 }
756 /**
757 * Returns the visitor country based on the Browser 'accepted language'
758 * information, but provides a hook for geolocation via IP address.
759 *
760 * @param string $lang browser lang
761 * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
762 * @param string $ip
763 * @return string 2 letter ISO code
764 */
765 public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
766 {
767 if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
768 return self::LANGUAGE_CODE_INVALID;
769 }
770 /** @var RegionDataProvider $dataProvider */
771 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
772 $validCountries = $dataProvider->getCountryList();
773 return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
774 }
775 /**
776 * Returns list of valid country codes
777 *
778 * @param string $browserLanguage
779 * @param array $validCountries Array of valid countries
780 * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
781 * @return array Array of 2 letter ISO codes
782 */
783 public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
784 {
785 /** @var LanguageDataProvider $dataProvider */
786 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
787 $langToCountry = $dataProvider->getLanguageToCountryList();
788 if ($enableLanguageToCountryGuess) {
789 if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
790 // match language (without region) to infer the country of origin
791 if (array_key_exists($matches[1], $langToCountry)) {
792 return $langToCountry[$matches[1]];
793 }
794 }
795 }
796 if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, \PREG_SET_ORDER)) {
797 foreach ($matches as $parts) {
798 // match location; we don't make any inferences from the language
799 if (array_key_exists($parts[1], $validCountries)) {
800 return $parts[1];
801 }
802 }
803 }
804 return self::LANGUAGE_CODE_INVALID;
805 }
806 /**
807 * Returns the language string, based only on the Browser 'accepted language' information.
808 * * The language tag is defined by ISO 639-1
809 *
810 * @param string $browserLanguage Browser's accepted language header
811 * @param array $validLanguages array of valid language codes
812 * @return string 2 letter ISO 639 code 'es' (Spanish)
813 */
814 public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
815 {
816 $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
817 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
818 if (strlen($languageRegionCode) === 2) {
819 $languageCode = $languageRegionCode;
820 } else {
821 $languageCode = substr($languageRegionCode, 0, 2);
822 }
823 if (in_array($languageCode, $validLanguages)) {
824 return $languageCode;
825 }
826 return self::LANGUAGE_CODE_INVALID;
827 }
828 /**
829 * Returns the language and region string, based only on the Browser 'accepted language' information.
830 * * The language tag is defined by ISO 639-1
831 * * The region tag is defined by ISO 3166-1
832 *
833 * @param string $browserLanguage Browser's accepted language header
834 * @param array $validLanguages array of valid language/region codes.
835 * @return string 2-letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
836 */
837 public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
838 {
839 $forceRegionValidation = !empty($validLanguages);
840 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
841 if (!preg_match_all('/(?:^|,)([a-z]{2,3})(?:[-][a-z]{4})?([-][a-z]{2})?/', $browserLanguage, $matches, \PREG_SET_ORDER)) {
842 return self::LANGUAGE_CODE_INVALID;
843 }
844 foreach ($matches as $parts) {
845 if (count($parts) < 2) {
846 continue;
847 }
848 $langIso639 = $parts[1];
849 // If a region tag is found eg. "fr-ca"
850 if (count($parts) === 3) {
851 $regionIso3166 = $parts[2];
852 // eg. "-ca"
853 if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
854 return $langIso639 . $regionIso3166;
855 }
856 // if a set of valid codes was provided, we do not append the region if it was not included
857 if (in_array($langIso639, $validLanguages) && !$forceRegionValidation) {
858 return $langIso639 . $regionIso3166;
859 }
860 }
861 // eg. "fr" or "es"
862 if (in_array($langIso639, $validLanguages)) {
863 return $langIso639;
864 }
865 }
866 return self::LANGUAGE_CODE_INVALID;
867 }
868 /**
869 * Returns the continent of a given country
870 *
871 * @param string $country 2 letters iso code
872 *
873 * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
874 */
875 public static function getContinent($country)
876 {
877 /** @var RegionDataProvider $dataProvider */
878 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
879 $countryList = $dataProvider->getCountryList();
880 if ($country === 'ti') {
881 $country = 'cn';
882 }
883 return isset($countryList[$country]) ? $countryList[$country] : 'unk';
884 }
885 /*
886 * Campaign
887 */
888 /**
889 * Returns the list of Campaign parameter names that will be read to classify
890 * a visit as coming from a Campaign
891 *
892 * @return array array(
893 * 0 => array( ... ) // campaign names parameters
894 * 1 => array( ... ) // campaign keyword parameters
895 * );
896 */
897 public static function getCampaignParameters(?int $idSite = null, bool $skipCompliancePolicyCheck = \false)
898 {
899 if (!$skipCompliancePolicyCheck) {
900 $featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
901 if ($featureFlagManager->isFeatureActive(PrivacyCompliance::class)) {
902 $cache = TrackerCache::getCacheWebsiteAttributes($idSite);
903 $cacheKey = CampaignTrackingParametersDisabled::class;
904 if (($cache[$cacheKey] ?? \false) === \true) {
905 return [[], []];
906 }
907 }
908 }
909 $return = array(\Piwik\Config::getInstance()->Tracker['campaign_var_name'], \Piwik\Config::getInstance()->Tracker['campaign_keyword_var_name']);
910 foreach ($return as &$list) {
911 if (strpos($list, ',') !== \false) {
912 $list = explode(',', $list);
913 } else {
914 $list = array($list);
915 }
916 $list = array_map('trim', $list);
917 }
918 return $return;
919 }
920 /*
921 * Referrer
922 */
923 /**
924 * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
925 * to fill the `IN (...)` part of a query.
926 *
927 * @param array|string $fields The names of the mysql table fields to bind, e.g.
928 * `array(fieldName1, fieldName2, fieldName3)`.
929 *
930 * _Note: The content of the array isn't important, just its length._
931 * @return string The placeholder string, e.g. `"?, ?, ?"`.
932 * @api
933 */
934 public static function getSqlStringFieldsArray($fields)
935 {
936 if (is_string($fields)) {
937 $fields = array($fields);
938 }
939 $count = count($fields);
940 if ($count === 0) {
941 return "''";
942 }
943 return '?' . str_repeat(',?', $count - 1);
944 }
945 /**
946 * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
947 * If for instance a German locale is used it would be a comma otherwise.
948 *
949 * @param float|string $value
950 * @return string
951 */
952 public static function forceDotAsSeparatorForDecimalPoint($value)
953 {
954 if (null === $value || \false === $value) {
955 return $value;
956 }
957 return str_replace(',', '.', $value);
958 }
959 /**
960 * Sets outgoing header.
961 *
962 * @param string $header The header.
963 * @param bool $replace Whether to replace existing or not.
964 */
965 public static function sendHeader($header, $replace = \true)
966 {
967 if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
968 if (strpos($header, ':') !== \false) {
969 [$headerName, $headerValue] = explode(':', $header, 2);
970 } else {
971 $headerName = $header;
972 $headerValue = '';
973 }
974 if (!array_key_exists($headerName, self::$headersSentInTests) || $replace) {
975 self::$headersSentInTests[$headerName] = $headerValue;
976 }
977 }
978 // don't send header in CLI mode
979 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
980 header($header, $replace);
981 }
982 }
983 /**
984 * Strips outgoing header.
985 *
986 * @param string $name The header name.
987 */
988 public static function stripHeader($name)
989 {
990 if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
991 unset(self::$headersSentInTests[$name]);
992 }
993 // don't strip header in CLI mode
994 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
995 header_remove($name);
996 }
997 }
998 /**
999 * Sends the given response code if supported.
1000 *
1001 * @param int $code Eg 204
1002 *
1003 * @throws Exception
1004 */
1005 public static function sendResponseCode($code)
1006 {
1007 $messages = array(200 => 'Ok', 204 => 'No Response', 301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified', 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 503 => 'Service Unavailable');
1008 if (!array_key_exists($code, $messages)) {
1009 throw new Exception('Response code not supported: ' . $code);
1010 }
1011 if (strpos(\PHP_SAPI, '-fcgi') === \false) {
1012 $key = 'HTTP/1.1';
1013 if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && strlen($_SERVER['SERVER_PROTOCOL']) < 15 && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
1014 $key = $_SERVER['SERVER_PROTOCOL'];
1015 }
1016 } else {
1017 // FastCGI
1018 $key = 'Status:';
1019 }
1020 $message = $messages[$code];
1021 \Piwik\Common::sendHeader($key . ' ' . $code . ' ' . $message);
1022 }
1023 /**
1024 * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
1025 * the Tracker cache.
1026 */
1027 public static function getCurrentLocationProviderId()
1028 {
1029 $cache = TrackerCache::getCacheGeneral();
1030 return empty($cache['currentLocationProviderId']) ? \Piwik\Plugins\UserCountry\LocationProvider::getDefaultProviderId() : $cache['currentLocationProviderId'];
1031 }
1032 /**
1033 * Marks an orphaned object for garbage collection.
1034 *
1035 * For more information: {@link https://github.com/piwik/piwik/issues/374}
1036 * @param mixed $var The object to destroy.
1037 * @api
1038 */
1039 public static function destroy(&$var)
1040 {
1041 if (is_object($var) && method_exists($var, '__destruct')) {
1042 $var->__destruct();
1043 }
1044 unset($var);
1045 $var = null;
1046 }
1047 /**
1048 * @deprecated Use the logger directly instead.
1049 */
1050 public static function printDebug($info = '')
1051 {
1052 if (is_object($info)) {
1053 $info = var_export($info, \true);
1054 }
1055 $logger = StaticContainer::get(LoggerInterface::class);
1056 if (is_array($info) || is_object($info)) {
1057 $out = var_export($info, \true);
1058 $logger->debug($out);
1059 } else {
1060 $logger->debug($info);
1061 }
1062 }
1063 /**
1064 * Returns true if the request is an AJAX request.
1065 *
1066 * @return bool
1067 */
1068 public static function isXmlHttpRequest()
1069 {
1070 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
1071 }
1072 /**
1073 * @param $validLanguages
1074 * @return array
1075 */
1076 protected static function checkValidLanguagesIsSet($validLanguages)
1077 {
1078 /** @var LanguageDataProvider $dataProvider */
1079 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
1080 if (empty($validLanguages)) {
1081 $validLanguages = array_keys($dataProvider->getLanguageList());
1082 return $validLanguages;
1083 }
1084 return $validLanguages;
1085 }
1086 /**
1087 * Flatten variously nested arrays into a single flat list of values
1088 *
1089 * @param array $array
1090 * @return array
1091 */
1092 public static function flattenArray(array $array) : array
1093 {
1094 $result = [];
1095 foreach ($array as $value) {
1096 if (is_array($value)) {
1097 $result = array_merge($result, static::flattenArray($value));
1098 } else {
1099 $result[] = $value;
1100 }
1101 }
1102 return $result;
1103 }
1104 }
1105