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