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