PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / 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 2 years 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 2 years 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
1078 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 static $prefixTable = null;
95 if (is_null($prefixTable)) {
96 $prefixTable = \Piwik\Config::getInstance()->database['tables_prefix'];
97 }
98 if (empty($prefixTable) || strpos($table, $prefixTable) !== 0) {
99 return $table;
100 }
101 $count = 1;
102 return str_replace($prefixTable, '', $table, $count);
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 * @param string $stringA
245 * @param string $stringB
246 * @return bool
247 */
248 public static function hashEquals(string $stringA, string $stringB)
249 {
250 if (function_exists('hash_equals')) {
251 return hash_equals($stringA, $stringB);
252 }
253 if (strlen($stringA) !== strlen($stringB)) {
254 return \false;
255 }
256 $result = "\x00";
257 $stringA ^= $stringB;
258 for ($i = 0; $i < strlen($stringA); $i++) {
259 $result |= $stringA[$i];
260 }
261 return $result === "\x00";
262 }
263 /**
264 * Secure wrapper for unserialize, which by default disallows unserializing classes
265 *
266 * @param string $string String to unserialize
267 * @param array $allowedClasses Class names that should be allowed to unserialize
268 * @param bool $rethrow Whether to rethrow exceptions or not.
269 * @return mixed
270 */
271 public static function safe_unserialize($string, $allowedClasses = [], $rethrow = \false)
272 {
273 try {
274 // phpcs:ignore Generic.PHP.ForbiddenFunctions
275 return unserialize($string ?? '', ['allowed_classes' => empty($allowedClasses) ? \false : $allowedClasses]);
276 } catch (\Throwable $e) {
277 if ($rethrow) {
278 throw $e;
279 }
280 $logger = StaticContainer::get(LoggerInterface::class);
281 $logger->debug('Unable to unserialize a string: {exception} (string = {string})', ['exception' => $e, 'string' => $string]);
282 return \false;
283 }
284 }
285 /*
286 * Escaping input
287 */
288 /**
289 * Sanitizes a string to help avoid XSS vulnerabilities.
290 *
291 * This function is automatically called when {@link getRequestVar()} is called,
292 * so you should not normally have to use it.
293 *
294 * This function should be used when outputting data that isn't escaped and was
295 * obtained from the user (for example when using the `|raw` twig filter on goal names).
296 *
297 * _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders
298 * should still be used._
299 *
300 * **Implementation Details**
301 *
302 * - [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php) is used to escape text.
303 * - Single quotes are not escaped so **Piwik's amazing community** will still be
304 * **Piwik's amazing community**.
305 * - Use of the `magic_quotes` setting will not break this method.
306 * - Boolean, numeric and null values are not modified.
307 *
308 * @param mixed $value The variable to be sanitized. If an array is supplied, the contents
309 * of the array will be sanitized recursively. The keys of the array
310 * will also be sanitized.
311 * @param bool $alreadyStripslashed Implementation detail, ignore.
312 * @throws Exception If `$value` is of an incorrect type.
313 * @return mixed The sanitized value.
314 * @api
315 */
316 public static function sanitizeInputValues($value, $alreadyStripslashed = \false)
317 {
318 if (is_numeric($value)) {
319 return $value;
320 } elseif (is_string($value)) {
321 $value = self::sanitizeString($value);
322 } elseif (is_array($value)) {
323 foreach (array_keys($value) as $key) {
324 $newKey = $key;
325 $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
326 if ($key !== $newKey) {
327 $value[$newKey] = $value[$key];
328 unset($value[$key]);
329 }
330 $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
331 }
332 } elseif (!is_null($value) && !is_bool($value)) {
333 throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, \true));
334 }
335 return $value;
336 }
337 /**
338 * Sanitize a single input value and removes line breaks, tabs and null characters.
339 *
340 * @param string $value
341 * @return string sanitized input
342 */
343 public static function sanitizeInputValue($value)
344 {
345 $value = self::sanitizeLineBreaks($value);
346 $value = self::sanitizeString($value);
347 return $value;
348 }
349 /**
350 * Sanitize a single input value
351 *
352 * @param $value
353 * @return string
354 */
355 private static function sanitizeString($value)
356 {
357 // $_GET and $_REQUEST already urldecode()'d
358 // decode
359 // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
360 $value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
361 $value = self::sanitizeNullBytes($value);
362 // escape
363 $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
364 // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
365 if ($value !== '' && $tmp === '') {
366 // convert and escape
367 $value = utf8_encode($value);
368 $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
369 return $tmp;
370 }
371 return $tmp;
372 }
373 /**
374 * Unsanitizes a single input value and returns the result.
375 *
376 * @param string $value
377 * @return string unsanitized input
378 * @api
379 */
380 public static function unsanitizeInputValue($value)
381 {
382 return htmlspecialchars_decode($value ?? '', self::HTML_ENCODING_QUOTE_STYLE);
383 }
384 /**
385 * Unsanitizes one or more values and returns the result.
386 *
387 * This method should be used when you need to unescape data that was obtained from
388 * the user.
389 *
390 * Some data in Piwik is stored sanitized (such as site name). In this case you may
391 * have to use this method to unsanitize it in order to, for example, output it in JSON.
392 *
393 * @param string|array $value The data to unsanitize. If an array is passed, the
394 * array is sanitized recursively. Key values are not unsanitized.
395 * @return string|array The unsanitized data.
396 * @api
397 */
398 public static function unsanitizeInputValues($value)
399 {
400 if (is_array($value)) {
401 $result = array();
402 foreach ($value as $key => $arrayValue) {
403 $result[$key] = self::unsanitizeInputValues($arrayValue);
404 }
405 return $result;
406 } else {
407 return self::unsanitizeInputValue($value);
408 }
409 }
410 /**
411 * @param string $value
412 * @return string Line breaks and line carriage removed
413 */
414 public static function sanitizeLineBreaks($value)
415 {
416 return is_null($value) ? '' : str_replace(array("\n", "\r"), '', $value);
417 }
418 /**
419 * @param string $value
420 * @return string Null bytes removed
421 */
422 public static function sanitizeNullBytes($value)
423 {
424 return str_replace(array("\x00"), '', $value);
425 }
426 /**
427 * Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals.
428 *
429 * Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._**
430 *
431 * If the variable cannot be found, and a default value was not provided, an exception is raised.
432 *
433 * _See {@link sanitizeInputValues()} to learn more about sanitization._
434 *
435 * @param string $varName Name of the request parameter to get. By default, we look in `$_GET[$varName]`
436 * and `$_POST[$varName]` for the value.
437 * @param string|null $varDefault The value to return if the request parameter cannot be found or has an empty value.
438 * @param string|null $varType Expected type of the request variable. This parameters value must be one of the following:
439 * `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`.
440 *
441 * If `'json'`, the string value will be `json_decode`-d and then sanitized.
442 * @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`.
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 * @return mixed The sanitized request parameter.
446 * @see Request::getParameter()
447 * @deprecated Use Request class instead, which will return raw values instead.
448 * @api
449 */
450 public static function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null)
451 {
452 if (is_null($requestArrayToUse)) {
453 $requestArrayToUse = $_GET + $_POST;
454 }
455 $varDefault = self::sanitizeInputValues($varDefault);
456 if ($varType === 'int') {
457 // settype accepts only integer
458 // 'int' is simply a shortcut for 'integer'
459 $varType = 'integer';
460 }
461 // there is no value $varName in the REQUEST so we try to use the default value
462 if (empty($varName) || !isset($requestArrayToUse[$varName]) || !is_array($requestArrayToUse[$varName]) && strlen($requestArrayToUse[$varName]) === 0) {
463 if (is_null($varDefault)) {
464 throw new Exception("The parameter '{$varName}' isn't set in the Request, and a default value wasn't provided.");
465 } else {
466 if (!is_null($varType) && in_array($varType, array('string', 'integer', 'array'))) {
467 settype($varDefault, $varType);
468 }
469 return $varDefault;
470 }
471 }
472 // Normal case, there is a value available in REQUEST for the requested varName:
473 // we deal w/ json differently
474 if ($varType === 'json') {
475 $value = $requestArrayToUse[$varName];
476 if (is_string($value)) {
477 $value = json_decode($value, $assoc = \true);
478 }
479 return self::sanitizeInputValues($value, \true);
480 }
481 $value = self::sanitizeInputValues($requestArrayToUse[$varName]);
482 if (isset($varType)) {
483 $ok = \false;
484 if ($varType === 'string') {
485 if (is_string($value) || is_int($value)) {
486 $ok = \true;
487 } elseif (is_float($value)) {
488 $value = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($value);
489 $ok = \true;
490 }
491 } elseif ($varType === 'integer') {
492 if ($value == (string) (int) $value) {
493 $ok = \true;
494 }
495 } elseif ($varType === 'float') {
496 $valueToCompare = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($value);
497 // Simplified regex for float without support for underscore notation
498 // will match: 1.234, 1.2e3, 7E-10
499 // won't match: 1_234.567
500 $floatRegex = "/^[+-]?((([0-9]+)|(([0-9]+)?\\.([0-9]+))|(([0-9]+)\\.([0-9]+)?))([eE][+-]?([0-9]+))?)\$/";
501 if (preg_match($floatRegex, $valueToCompare)) {
502 $ok = \true;
503 }
504 } elseif ($varType === 'array') {
505 if (is_array($value)) {
506 $ok = \true;
507 }
508 } else {
509 throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
510 }
511 // The type is not correct
512 if ($ok === \false) {
513 if ($varDefault === null) {
514 throw new Exception("The parameter '{$varName}' doesn't have a correct type, and a default value wasn't provided.");
515 } else {
516 // we return the default value with the good type set
517 settype($varDefault, $varType);
518 return $varDefault;
519 }
520 }
521 settype($value, $varType);
522 }
523 return $value;
524 }
525 /*
526 * Generating unique strings
527 */
528 /**
529 * Generates a random integer
530 *
531 * @param int $min
532 * @param null|int $max Defaults to max int value
533 * @return int
534 */
535 public static function getRandomInt($min = 0, $max = null)
536 {
537 if (!isset($max)) {
538 $max = \PHP_INT_MAX;
539 }
540 return random_int($min, $max);
541 }
542 /**
543 * Returns a 32 characters long uniq ID
544 *
545 * @return string 32 chars
546 */
547 public static function generateUniqId()
548 {
549 return bin2hex(random_bytes(16));
550 }
551 /**
552 * Configurable hash() algorithm (defaults to md5)
553 *
554 * @param string $str String to be hashed
555 * @param bool $raw_output
556 * @return string Hash string
557 */
558 public static function hash($str, $raw_output = \false)
559 {
560 static $hashAlgorithm = null;
561 if (is_null($hashAlgorithm)) {
562 $hashAlgorithm = @\Piwik\Config::getInstance()->General['hash_algorithm'];
563 }
564 if ($hashAlgorithm) {
565 $hash = @hash($hashAlgorithm, $str, $raw_output);
566 if ($hash !== \false) {
567 return $hash;
568 }
569 }
570 return md5($str, $raw_output);
571 }
572 /**
573 * Generate random string.
574 *
575 * @param int $length string length
576 * @param string $alphabet characters allowed in random string
577 * @return string random string with given length
578 */
579 public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
580 {
581 $chars = $alphabet;
582 $str = '';
583 for ($i = 0; $i < $length; $i++) {
584 $rand_key = self::getRandomInt(0, strlen($chars) - 1);
585 $str .= substr($chars, $rand_key, 1);
586 }
587 return str_shuffle($str);
588 }
589 /*
590 * Conversions
591 */
592 /**
593 * Convert hexadecimal representation into binary data.
594 * !! Will emit warning if input string is not hex!!
595 *
596 * @see http://php.net/bin2hex
597 *
598 * @param string $str Hexadecimal representation
599 * @return string
600 */
601 public static function hex2bin($str)
602 {
603 return pack("H*", $str);
604 }
605 /**
606 * This function will convert the input string to the binary representation of the ID
607 * but it will throw an Exception if the specified input ID is not correct
608 *
609 * This is used when building segments containing visitorId which could be an invalid string
610 * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
611 *
612 * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
613 * so better be safe and get the php error when something unexpected is happening
614 * @param string $id
615 * @throws Exception
616 * @return string binary string
617 */
618 public static function convertVisitorIdToBin($id)
619 {
620 if (strlen($id) !== \Piwik\Tracker::LENGTH_HEX_ID_STRING || @bin2hex(self::hex2bin($id)) != $id) {
621 throw new Exception("visitorId is expected to be a " . \Piwik\Tracker::LENGTH_HEX_ID_STRING . " hex char string");
622 }
623 return self::hex2bin($id);
624 }
625 /**
626 * Converts a User ID string to the Visitor ID Binary representation.
627 *
628 * @param $userId
629 * @return string
630 */
631 public static function convertUserIdToVisitorIdBin($userId)
632 {
633 $userIdHashed = \MatomoTracker::getUserIdHashed($userId);
634 return self::convertVisitorIdToBin($userIdHashed);
635 }
636 /**
637 * Detects whether an error occurred during the last json encode/decode.
638 * @return bool
639 */
640 public static function hasJsonErrorOccurred()
641 {
642 return json_last_error() != \JSON_ERROR_NONE;
643 }
644 /**
645 * Returns a human readable error message in case an error occurred during the last json encode/decode.
646 * Returns an empty string in case there was no error.
647 *
648 * @return string
649 */
650 public static function getLastJsonError()
651 {
652 switch (json_last_error()) {
653 case \JSON_ERROR_NONE:
654 return '';
655 case \JSON_ERROR_DEPTH:
656 return 'Maximum stack depth exceeded';
657 case \JSON_ERROR_STATE_MISMATCH:
658 return 'Underflow or the modes mismatch';
659 case \JSON_ERROR_CTRL_CHAR:
660 return 'Unexpected control character found';
661 case \JSON_ERROR_SYNTAX:
662 return 'Syntax error, malformed JSON';
663 case \JSON_ERROR_UTF8:
664 return 'Malformed UTF-8 characters, possibly incorrectly encoded';
665 }
666 return 'Unknown error';
667 }
668 public static function stringEndsWith($haystack, $needle)
669 {
670 if (strlen(strval($needle)) === 0) {
671 return \true;
672 }
673 if (strlen(strval($haystack)) === 0) {
674 return \false;
675 }
676 $lastCharacters = substr($haystack, -strlen($needle));
677 return $lastCharacters === $needle;
678 }
679 /**
680 * Returns the list of parent classes for the given class.
681 *
682 * @param string $class A class name.
683 * @return string[] The list of parent classes in order from highest ancestor to the descended class.
684 */
685 public static function getClassLineage($class)
686 {
687 $classes = array_merge(array($class), array_values(class_parents($class, $autoload = \false)));
688 return array_reverse($classes);
689 }
690 /*
691 * DataFiles
692 */
693 /**
694 * Returns list of provider names
695 *
696 * @see core/DataFiles/Providers.php
697 *
698 * @return array Array of ( dnsName => providerName )
699 */
700 public static function getProviderNames()
701 {
702 require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
703 $providers = $GLOBALS['Piwik_ProviderNames'];
704 return $providers;
705 }
706 /*
707 * Language, country, continent
708 */
709 /**
710 * Returns the browser language code, eg. "en-gb,en;q=0.5"
711 *
712 * @param string|null $browserLang Optional browser language, otherwise taken from the request header
713 * @return string
714 */
715 public static function getBrowserLanguage($browserLang = null)
716 {
717 static $replacementPatterns = array(
718 // extraneous bits of RFC 3282 that we ignore
719 '/(\\\\.)/',
720 // quoted-pairs
721 '/(\\s+)/',
722 // CFWcS white space
723 '/(\\([^)]*\\))/',
724 // CFWS comments
725 '/(;q=[0-9.]+)/',
726 // quality
727 // found in the LANG environment variable
728 '/\\.(.*)/',
729 // charset (e.g., en_CA.UTF-8)
730 '/^C$/',
731 );
732 if (is_null($browserLang)) {
733 $browserLang = self::sanitizeInputValues($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
734 if (empty($browserLang) && self::isPhpCliMode()) {
735 $browserLang = @getenv('LANG');
736 }
737 }
738 if (empty($browserLang)) {
739 // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
740 $browserLang = "";
741 } else {
742 // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
743 // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
744 // but we guard against a bad user agent which naively uses its locale
745 $browserLang = strtolower(str_replace('_', '-', $browserLang));
746 // filters
747 $browserLang = preg_replace($replacementPatterns, '', $browserLang);
748 $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1);
749 // Firefox bug
750 $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1);
751 // unregistered language tag
752 $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang);
753 // unofficial (proposed) code in the wild
754 }
755 return $browserLang;
756 }
757 /**
758 * Returns the visitor country based on the Browser 'accepted language'
759 * information, but provides a hook for geolocation via IP address.
760 *
761 * @param string $lang browser lang
762 * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
763 * @param string $ip
764 * @return string 2 letter ISO code
765 */
766 public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
767 {
768 if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
769 return self::LANGUAGE_CODE_INVALID;
770 }
771 /** @var RegionDataProvider $dataProvider */
772 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
773 $validCountries = $dataProvider->getCountryList();
774 return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
775 }
776 /**
777 * Returns list of valid country codes
778 *
779 * @param string $browserLanguage
780 * @param array $validCountries Array of valid countries
781 * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
782 * @return array Array of 2 letter ISO codes
783 */
784 public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
785 {
786 /** @var LanguageDataProvider $dataProvider */
787 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
788 $langToCountry = $dataProvider->getLanguageToCountryList();
789 if ($enableLanguageToCountryGuess) {
790 if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
791 // match language (without region) to infer the country of origin
792 if (array_key_exists($matches[1], $langToCountry)) {
793 return $langToCountry[$matches[1]];
794 }
795 }
796 }
797 if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, \PREG_SET_ORDER)) {
798 foreach ($matches as $parts) {
799 // match location; we don't make any inferences from the language
800 if (array_key_exists($parts[1], $validCountries)) {
801 return $parts[1];
802 }
803 }
804 }
805 return self::LANGUAGE_CODE_INVALID;
806 }
807 /**
808 * Returns the language string, based only on the Browser 'accepted language' information.
809 * * The language tag is defined by ISO 639-1
810 *
811 * @param string $browserLanguage Browser's accepted language header
812 * @param array $validLanguages array of valid language codes
813 * @return string 2 letter ISO 639 code 'es' (Spanish)
814 */
815 public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
816 {
817 $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
818 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
819 if (strlen($languageRegionCode) === 2) {
820 $languageCode = $languageRegionCode;
821 } else {
822 $languageCode = substr($languageRegionCode, 0, 2);
823 }
824 if (in_array($languageCode, $validLanguages)) {
825 return $languageCode;
826 }
827 return self::LANGUAGE_CODE_INVALID;
828 }
829 /**
830 * Returns the language and region string, based only on the Browser 'accepted language' information.
831 * * The language tag is defined by ISO 639-1
832 * * The region tag is defined by ISO 3166-1
833 *
834 * @param string $browserLanguage Browser's accepted language header
835 * @param array $validLanguages array of valid language/region codes.
836 * @return string 2-letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
837 */
838 public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
839 {
840 $forceRegionValidation = !empty($validLanguages);
841 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
842 if (!preg_match_all('/(?:^|,)([a-z]{2,3})(?:[-][a-z]{4})?([-][a-z]{2})?/', $browserLanguage, $matches, \PREG_SET_ORDER)) {
843 return self::LANGUAGE_CODE_INVALID;
844 }
845 foreach ($matches as $parts) {
846 $langIso639 = $parts[1];
847 if (empty($langIso639)) {
848 continue;
849 }
850 // If a region tag is found eg. "fr-ca"
851 if (count($parts) === 3) {
852 $regionIso3166 = $parts[2];
853 // eg. "-ca"
854 if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
855 return $langIso639 . $regionIso3166;
856 }
857 // if a set of valid codes was provided, we do not append the region if it was not included
858 if (in_array($langIso639, $validLanguages) && !$forceRegionValidation) {
859 return $langIso639 . $regionIso3166;
860 }
861 }
862 // eg. "fr" or "es"
863 if (in_array($langIso639, $validLanguages)) {
864 return $langIso639;
865 }
866 }
867 return self::LANGUAGE_CODE_INVALID;
868 }
869 /**
870 * Returns the continent of a given country
871 *
872 * @param string $country 2 letters iso code
873 *
874 * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
875 */
876 public static function getContinent($country)
877 {
878 /** @var RegionDataProvider $dataProvider */
879 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
880 $countryList = $dataProvider->getCountryList();
881 if ($country === 'ti') {
882 $country = 'cn';
883 }
884 return isset($countryList[$country]) ? $countryList[$country] : 'unk';
885 }
886 /*
887 * Campaign
888 */
889 /**
890 * Returns the list of Campaign parameter names that will be read to classify
891 * a visit as coming from a Campaign
892 *
893 * @return array array(
894 * 0 => array( ... ) // campaign names parameters
895 * 1 => array( ... ) // campaign keyword parameters
896 * );
897 */
898 public static function getCampaignParameters()
899 {
900 $return = array(\Piwik\Config::getInstance()->Tracker['campaign_var_name'], \Piwik\Config::getInstance()->Tracker['campaign_keyword_var_name']);
901 foreach ($return as &$list) {
902 if (strpos($list, ',') !== \false) {
903 $list = explode(',', $list);
904 } else {
905 $list = array($list);
906 }
907 $list = array_map('trim', $list);
908 }
909 return $return;
910 }
911 /*
912 * Referrer
913 */
914 /**
915 * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
916 * to fill the `IN (...)` part of a query.
917 *
918 * @param array|string $fields The names of the mysql table fields to bind, e.g.
919 * `array(fieldName1, fieldName2, fieldName3)`.
920 *
921 * _Note: The content of the array isn't important, just its length._
922 * @return string The placeholder string, e.g. `"?, ?, ?"`.
923 * @api
924 */
925 public static function getSqlStringFieldsArray($fields)
926 {
927 if (is_string($fields)) {
928 $fields = array($fields);
929 }
930 $count = count($fields);
931 if ($count === 0) {
932 return "''";
933 }
934 return '?' . str_repeat(',?', $count - 1);
935 }
936 /**
937 * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
938 * If for instance a German locale is used it would be a comma otherwise.
939 *
940 * @param float|string $value
941 * @return string
942 */
943 public static function forceDotAsSeparatorForDecimalPoint($value)
944 {
945 if (null === $value || \false === $value) {
946 return $value;
947 }
948 return str_replace(',', '.', $value);
949 }
950 /**
951 * Sets outgoing header.
952 *
953 * @param string $header The header.
954 * @param bool $replace Whether to replace existing or not.
955 */
956 public static function sendHeader($header, $replace = \true)
957 {
958 if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
959 if (strpos($header, ':') !== \false) {
960 [$headerName, $headerValue] = explode(':', $header, 2);
961 } else {
962 $headerName = $header;
963 $headerValue = '';
964 }
965 if (!array_key_exists($headerName, self::$headersSentInTests) || $replace) {
966 self::$headersSentInTests[$headerName] = $headerValue;
967 }
968 }
969 // don't send header in CLI mode
970 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
971 header($header, $replace);
972 }
973 }
974 /**
975 * Strips outgoing header.
976 *
977 * @param string $name The header name.
978 */
979 public static function stripHeader($name)
980 {
981 if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
982 unset(self::$headersSentInTests[$name]);
983 }
984 // don't strip header in CLI mode
985 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
986 header_remove($name);
987 }
988 }
989 /**
990 * Sends the given response code if supported.
991 *
992 * @param int $code Eg 204
993 *
994 * @throws Exception
995 */
996 public static function sendResponseCode($code)
997 {
998 $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');
999 if (!array_key_exists($code, $messages)) {
1000 throw new Exception('Response code not supported: ' . $code);
1001 }
1002 if (strpos(\PHP_SAPI, '-fcgi') === \false) {
1003 $key = 'HTTP/1.1';
1004 if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && strlen($_SERVER['SERVER_PROTOCOL']) < 15 && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
1005 $key = $_SERVER['SERVER_PROTOCOL'];
1006 }
1007 } else {
1008 // FastCGI
1009 $key = 'Status:';
1010 }
1011 $message = $messages[$code];
1012 \Piwik\Common::sendHeader($key . ' ' . $code . ' ' . $message);
1013 }
1014 /**
1015 * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
1016 * the Tracker cache.
1017 */
1018 public static function getCurrentLocationProviderId()
1019 {
1020 $cache = TrackerCache::getCacheGeneral();
1021 return empty($cache['currentLocationProviderId']) ? \Piwik\Plugins\UserCountry\LocationProvider::getDefaultProviderId() : $cache['currentLocationProviderId'];
1022 }
1023 /**
1024 * Marks an orphaned object for garbage collection.
1025 *
1026 * For more information: {@link https://github.com/piwik/piwik/issues/374}
1027 * @param mixed $var The object to destroy.
1028 * @api
1029 */
1030 public static function destroy(&$var)
1031 {
1032 if (is_object($var) && method_exists($var, '__destruct')) {
1033 $var->__destruct();
1034 }
1035 unset($var);
1036 $var = null;
1037 }
1038 /**
1039 * @deprecated Use the logger directly instead.
1040 */
1041 public static function printDebug($info = '')
1042 {
1043 if (is_object($info)) {
1044 $info = var_export($info, \true);
1045 }
1046 $logger = StaticContainer::get(LoggerInterface::class);
1047 if (is_array($info) || is_object($info)) {
1048 $out = var_export($info, \true);
1049 $logger->debug($out);
1050 } else {
1051 $logger->debug($info);
1052 }
1053 }
1054 /**
1055 * Returns true if the request is an AJAX request.
1056 *
1057 * @return bool
1058 */
1059 public static function isXmlHttpRequest()
1060 {
1061 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
1062 }
1063 /**
1064 * @param $validLanguages
1065 * @return array
1066 */
1067 protected static function checkValidLanguagesIsSet($validLanguages)
1068 {
1069 /** @var LanguageDataProvider $dataProvider */
1070 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
1071 if (empty($validLanguages)) {
1072 $validLanguages = array_keys($dataProvider->getLanguageList());
1073 return $validLanguages;
1074 }
1075 return $validLanguages;
1076 }
1077 }
1078