PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.3
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Common.php
matomo / app / core Last commit date
API 2 years ago Access 2 years ago Application 2 years ago Archive 2 years ago ArchiveProcessor 2 years ago Archiver 2 years ago AssetManager 2 years ago Auth 2 years ago Category 2 years ago Changes 2 years ago CliMulti 2 years ago Columns 2 years ago Concurrency 2 years ago Config 2 years ago Container 2 years ago CronArchive 2 years ago DataAccess 2 years ago DataFiles 2 years ago DataTable 2 years ago Db 2 years ago DeviceDetector 2 years ago Email 2 years ago Exception 2 years ago Http 2 years ago Intl 2 years ago Log 2 years ago Mail 2 years ago Measurable 2 years ago Menu 2 years ago Metrics 2 years ago Notification 2 years ago Period 2 years ago Plugin 2 years ago ProfessionalServices 2 years ago Report 2 years ago ReportRenderer 2 years ago Scheduler 2 years ago Segment 2 years ago Session 2 years ago Settings 2 years ago Tracker 2 years ago Translation 2 years ago Twig 2 years ago UpdateCheck 2 years ago Updater 2 years ago Updates 2 years ago Validators 2 years ago View 2 years ago ViewDataTable 2 years ago Visualization 2 years ago Widget 2 years ago .htaccess 2 years ago Access.php 2 years ago Archive.php 2 years ago ArchiveProcessor.php 2 years ago AssetManager.php 2 years ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 2 years ago CliMulti.php 2 years ago Common.php 2 years ago Config.php 2 years ago Console.php 2 years ago Context.php 2 years ago Cookie.php 2 years ago CronArchive.php 2 years ago DI.php 2 years ago DataArray.php 2 years ago DataTable.php 2 years ago Date.php 2 years ago Db.php 2 years ago DbHelper.php 2 years ago Development.php 2 years ago ErrorHandler.php 2 years ago EventDispatcher.php 2 years ago ExceptionHandler.php 2 years ago FileIntegrity.php 2 years ago Filechecks.php 2 years ago Filesystem.php 2 years ago FrontController.php 2 years ago Http.php 2 years ago IP.php 2 years ago Log.php 2 years ago LogDeleter.php 2 years ago Mail.php 2 years ago Metrics.php 2 years ago NoAccessException.php 2 years ago Nonce.php 2 years ago Notification.php 2 years ago NumberFormatter.php 2 years ago Option.php 2 years ago Period.php 2 years ago Piwik.php 2 years ago Plugin.php 2 years ago Profiler.php 2 years ago ProxyHeaders.php 2 years ago ProxyHttp.php 2 years ago QuickForm2.php 2 years ago RankingQuery.php 2 years ago ReportRenderer.php 2 years ago Request.php 2 years ago Segment.php 2 years ago Sequence.php 2 years ago Session.php 2 years ago SettingsPiwik.php 2 years ago SettingsServer.php 2 years ago Singleton.php 2 years ago Site.php 2 years ago SiteContentDetector.php 2 years ago SupportedBrowser.php 2 years ago TCPDF.php 2 years ago Theme.php 2 years ago Timer.php 2 years ago Tracker.php 2 years ago Twig.php 2 years ago Unzip.php 2 years ago UpdateCheck.php 2 years ago Updater.php 2 years ago UpdaterErrorException.php 2 years ago Updates.php 2 years ago Url.php 2 years ago UrlHelper.php 2 years ago Version.php 2 years ago View.php 2 years ago bootstrap.php 2 years ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
Common.php
1057 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 *
9 */
10 namespace Piwik;
11
12 use Exception;
13 use Piwik\CliMulti\Process;
14 use Piwik\Container\StaticContainer;
15 use Piwik\Intl\Data\Provider\LanguageDataProvider;
16 use Piwik\Intl\Data\Provider\RegionDataProvider;
17 use Piwik\Log\LoggerInterface;
18 use Piwik\Tracker\Cache as TrackerCache;
19 /**
20 * Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
21 *
22 * This is the only non-Tracker class loaded by the **\/piwik.php** file.
23 */
24 class Common
25 {
26 // constants used to map the referrer type to an integer in the log_visit table
27 const REFERRER_TYPE_DIRECT_ENTRY = 1;
28 const REFERRER_TYPE_SEARCH_ENGINE = 2;
29 const REFERRER_TYPE_WEBSITE = 3;
30 const REFERRER_TYPE_CAMPAIGN = 6;
31 const REFERRER_TYPE_SOCIAL_NETWORK = 7;
32 // Flag used with htmlspecialchar. See php.net/htmlspecialchars.
33 const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;
34 public static $isCliMode = null;
35 /*
36 * Database
37 */
38 const LANGUAGE_CODE_INVALID = 'xx';
39 /**
40 * Hashes a string into an integer which should be very low collision risks
41 * @param string $string String to hash
42 * @return int Resulting int hash
43 */
44 public static function hashStringToInt($string)
45 {
46 $stringHash = substr(md5($string), 0, 8);
47 return base_convert($stringHash, 16, 10);
48 }
49 /**
50 * Returns a prefixed table name.
51 *
52 * The table prefix is determined by the `[database] tables_prefix` INI config
53 * option.
54 *
55 * @param string $table The table name to prefix, ie "log_visit"
56 * @return string The prefixed name, ie "piwik-production_log_visit".
57 * @api
58 */
59 public static function prefixTable($table)
60 {
61 $prefix = \Piwik\Config::getInstance()->database['tables_prefix'];
62 return $prefix . $table;
63 }
64 /**
65 * Returns an array containing the prefixed table names of every passed argument.
66 *
67 * @param string ...$tables The table names to prefix, ie "log_visit"
68 * @return array The prefixed names in an array.
69 */
70 public static function prefixTables(...$tables)
71 {
72 $result = array();
73 foreach ($tables as $table) {
74 $result[] = self::prefixTable($table);
75 }
76 return $result;
77 }
78 /**
79 * Removes the prefix from a table name and returns the result.
80 *
81 * The table prefix is determined by the `[database] tables_prefix` INI config
82 * option.
83 *
84 * @param string $table The prefixed table name, eg "piwik-production_log_visit".
85 * @return string The unprefixed table name, eg "log_visit".
86 * @api
87 */
88 public static function unprefixTable($table)
89 {
90 static $prefixTable = null;
91 if (is_null($prefixTable)) {
92 $prefixTable = \Piwik\Config::getInstance()->database['tables_prefix'];
93 }
94 if (empty($prefixTable) || strpos($table, $prefixTable) !== 0) {
95 return $table;
96 }
97 $count = 1;
98 return str_replace($prefixTable, '', $table, $count);
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](http://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 = (string) (float) $value;
493 $valueToCompare = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($valueToCompare);
494 if ($value == $valueToCompare) {
495 $ok = true;
496 }
497 } elseif ($varType === 'array') {
498 if (is_array($value)) {
499 $ok = true;
500 }
501 } else {
502 throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
503 }
504 // The type is not correct
505 if ($ok === false) {
506 if ($varDefault === null) {
507 throw new Exception("The parameter '{$varName}' doesn't have a correct type, and a default value wasn't provided.");
508 } else {
509 // we return the default value with the good type set
510 settype($varDefault, $varType);
511 return $varDefault;
512 }
513 }
514 settype($value, $varType);
515 }
516 return $value;
517 }
518 /*
519 * Generating unique strings
520 */
521 /**
522 * Generates a random integer
523 *
524 * @param int $min
525 * @param null|int $max Defaults to max int value
526 * @return int
527 */
528 public static function getRandomInt($min = 0, $max = null)
529 {
530 if (!isset($max)) {
531 $max = PHP_INT_MAX;
532 }
533 return random_int($min, $max);
534 }
535 /**
536 * Returns a 32 characters long uniq ID
537 *
538 * @return string 32 chars
539 */
540 public static function generateUniqId()
541 {
542 return bin2hex(random_bytes(16));
543 }
544 /**
545 * Configurable hash() algorithm (defaults to md5)
546 *
547 * @param string $str String to be hashed
548 * @param bool $raw_output
549 * @return string Hash string
550 */
551 public static function hash($str, $raw_output = false)
552 {
553 static $hashAlgorithm = null;
554 if (is_null($hashAlgorithm)) {
555 $hashAlgorithm = @\Piwik\Config::getInstance()->General['hash_algorithm'];
556 }
557 if ($hashAlgorithm) {
558 $hash = @hash($hashAlgorithm, $str, $raw_output);
559 if ($hash !== false) {
560 return $hash;
561 }
562 }
563 return md5($str, $raw_output);
564 }
565 /**
566 * Generate random string.
567 *
568 * @param int $length string length
569 * @param string $alphabet characters allowed in random string
570 * @return string random string with given length
571 */
572 public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
573 {
574 $chars = $alphabet;
575 $str = '';
576 for ($i = 0; $i < $length; $i++) {
577 $rand_key = self::getRandomInt(0, strlen($chars) - 1);
578 $str .= substr($chars, $rand_key, 1);
579 }
580 return str_shuffle($str);
581 }
582 /*
583 * Conversions
584 */
585 /**
586 * Convert hexadecimal representation into binary data.
587 * !! Will emit warning if input string is not hex!!
588 *
589 * @see http://php.net/bin2hex
590 *
591 * @param string $str Hexadecimal representation
592 * @return string
593 */
594 public static function hex2bin($str)
595 {
596 return pack("H*", $str);
597 }
598 /**
599 * This function will convert the input string to the binary representation of the ID
600 * but it will throw an Exception if the specified input ID is not correct
601 *
602 * This is used when building segments containing visitorId which could be an invalid string
603 * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
604 *
605 * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
606 * so better be safe and get the php error when something unexpected is happening
607 * @param string $id
608 * @throws Exception
609 * @return string binary string
610 */
611 public static function convertVisitorIdToBin($id)
612 {
613 if (strlen($id) !== \Piwik\Tracker::LENGTH_HEX_ID_STRING || @bin2hex(self::hex2bin($id)) != $id) {
614 throw new Exception("visitorId is expected to be a " . \Piwik\Tracker::LENGTH_HEX_ID_STRING . " hex char string");
615 }
616 return self::hex2bin($id);
617 }
618 /**
619 * Converts a User ID string to the Visitor ID Binary representation.
620 *
621 * @param $userId
622 * @return string
623 */
624 public static function convertUserIdToVisitorIdBin($userId)
625 {
626 $userIdHashed = \MatomoTracker::getUserIdHashed($userId);
627 return self::convertVisitorIdToBin($userIdHashed);
628 }
629 /**
630 * Detects whether an error occurred during the last json encode/decode.
631 * @return bool
632 */
633 public static function hasJsonErrorOccurred()
634 {
635 return json_last_error() != JSON_ERROR_NONE;
636 }
637 /**
638 * Returns a human readable error message in case an error occurred during the last json encode/decode.
639 * Returns an empty string in case there was no error.
640 *
641 * @return string
642 */
643 public static function getLastJsonError()
644 {
645 switch (json_last_error()) {
646 case JSON_ERROR_NONE:
647 return '';
648 case JSON_ERROR_DEPTH:
649 return 'Maximum stack depth exceeded';
650 case JSON_ERROR_STATE_MISMATCH:
651 return 'Underflow or the modes mismatch';
652 case JSON_ERROR_CTRL_CHAR:
653 return 'Unexpected control character found';
654 case JSON_ERROR_SYNTAX:
655 return 'Syntax error, malformed JSON';
656 case JSON_ERROR_UTF8:
657 return 'Malformed UTF-8 characters, possibly incorrectly encoded';
658 }
659 return 'Unknown error';
660 }
661 public static function stringEndsWith($haystack, $needle)
662 {
663 if (strlen(strval($needle)) === 0) {
664 return true;
665 }
666 if (strlen(strval($haystack)) === 0) {
667 return false;
668 }
669 $lastCharacters = substr($haystack, -strlen($needle));
670 return $lastCharacters === $needle;
671 }
672 /**
673 * Returns the list of parent classes for the given class.
674 *
675 * @param string $class A class name.
676 * @return string[] The list of parent classes in order from highest ancestor to the descended class.
677 */
678 public static function getClassLineage($class)
679 {
680 $classes = array_merge(array($class), array_values(class_parents($class, $autoload = false)));
681 return array_reverse($classes);
682 }
683 /*
684 * DataFiles
685 */
686 /**
687 * Returns list of provider names
688 *
689 * @see core/DataFiles/Providers.php
690 *
691 * @return array Array of ( dnsName => providerName )
692 */
693 public static function getProviderNames()
694 {
695 require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
696 $providers = $GLOBALS['Piwik_ProviderNames'];
697 return $providers;
698 }
699 /*
700 * Language, country, continent
701 */
702 /**
703 * Returns the browser language code, eg. "en-gb,en;q=0.5"
704 *
705 * @param string|null $browserLang Optional browser language, otherwise taken from the request header
706 * @return string
707 */
708 public static function getBrowserLanguage($browserLang = null)
709 {
710 static $replacementPatterns = array(
711 // extraneous bits of RFC 3282 that we ignore
712 '/(\\\\.)/',
713 // quoted-pairs
714 '/(\\s+)/',
715 // CFWcS white space
716 '/(\\([^)]*\\))/',
717 // CFWS comments
718 '/(;q=[0-9.]+)/',
719 // quality
720 // found in the LANG environment variable
721 '/\\.(.*)/',
722 // charset (e.g., en_CA.UTF-8)
723 '/^C$/',
724 );
725 if (is_null($browserLang)) {
726 $browserLang = self::sanitizeInputValues($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
727 if (empty($browserLang) && self::isPhpCliMode()) {
728 $browserLang = @getenv('LANG');
729 }
730 }
731 if (empty($browserLang)) {
732 // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
733 $browserLang = "";
734 } else {
735 // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
736 // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
737 // but we guard against a bad user agent which naively uses its locale
738 $browserLang = strtolower(str_replace('_', '-', $browserLang));
739 // filters
740 $browserLang = preg_replace($replacementPatterns, '', $browserLang);
741 $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1);
742 // Firefox bug
743 $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1);
744 // unregistered language tag
745 $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang);
746 // unofficial (proposed) code in the wild
747 }
748 return $browserLang;
749 }
750 /**
751 * Returns the visitor country based on the Browser 'accepted language'
752 * information, but provides a hook for geolocation via IP address.
753 *
754 * @param string $lang browser lang
755 * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
756 * @param string $ip
757 * @return string 2 letter ISO code
758 */
759 public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
760 {
761 if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
762 return self::LANGUAGE_CODE_INVALID;
763 }
764 /** @var RegionDataProvider $dataProvider */
765 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
766 $validCountries = $dataProvider->getCountryList();
767 return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
768 }
769 /**
770 * Returns list of valid country codes
771 *
772 * @param string $browserLanguage
773 * @param array $validCountries Array of valid countries
774 * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
775 * @return array Array of 2 letter ISO codes
776 */
777 public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
778 {
779 /** @var LanguageDataProvider $dataProvider */
780 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
781 $langToCountry = $dataProvider->getLanguageToCountryList();
782 if ($enableLanguageToCountryGuess) {
783 if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
784 // match language (without region) to infer the country of origin
785 if (array_key_exists($matches[1], $langToCountry)) {
786 return $langToCountry[$matches[1]];
787 }
788 }
789 }
790 if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
791 foreach ($matches as $parts) {
792 // match location; we don't make any inferences from the language
793 if (array_key_exists($parts[1], $validCountries)) {
794 return $parts[1];
795 }
796 }
797 }
798 return self::LANGUAGE_CODE_INVALID;
799 }
800 /**
801 * Returns the language string, based only on the Browser 'accepted language' information.
802 * * The language tag is defined by ISO 639-1
803 *
804 * @param string $browserLanguage Browser's accepted language header
805 * @param array $validLanguages array of valid language codes
806 * @return string 2 letter ISO 639 code 'es' (Spanish)
807 */
808 public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
809 {
810 $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
811 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
812 if (strlen($languageRegionCode) === 2) {
813 $languageCode = $languageRegionCode;
814 } else {
815 $languageCode = substr($languageRegionCode, 0, 2);
816 }
817 if (in_array($languageCode, $validLanguages)) {
818 return $languageCode;
819 }
820 return self::LANGUAGE_CODE_INVALID;
821 }
822 /**
823 * Returns the language and region string, based only on the Browser 'accepted language' information.
824 * * The language tag is defined by ISO 639-1
825 * * The region tag is defined by ISO 3166-1
826 *
827 * @param string $browserLanguage Browser's accepted language header
828 * @param array $validLanguages array of valid language/region codes.
829 * @return string 2-letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
830 */
831 public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
832 {
833 $forceRegionValidation = !empty($validLanguages);
834 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
835 if (!preg_match_all('/(?:^|,)([a-z]{2,3})(?:[-][a-z]{4})?([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
836 return self::LANGUAGE_CODE_INVALID;
837 }
838 foreach ($matches as $parts) {
839 $langIso639 = $parts[1];
840 if (empty($langIso639)) {
841 continue;
842 }
843 // If a region tag is found eg. "fr-ca"
844 if (count($parts) === 3) {
845 $regionIso3166 = $parts[2];
846 // eg. "-ca"
847 if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
848 return $langIso639 . $regionIso3166;
849 }
850 // if a set of valid codes was provided, we do not append the region if it was not included
851 if (in_array($langIso639, $validLanguages) && !$forceRegionValidation) {
852 return $langIso639 . $regionIso3166;
853 }
854 }
855 // eg. "fr" or "es"
856 if (in_array($langIso639, $validLanguages)) {
857 return $langIso639;
858 }
859 }
860 return self::LANGUAGE_CODE_INVALID;
861 }
862 /**
863 * Returns the continent of a given country
864 *
865 * @param string $country 2 letters iso code
866 *
867 * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
868 */
869 public static function getContinent($country)
870 {
871 /** @var RegionDataProvider $dataProvider */
872 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
873 $countryList = $dataProvider->getCountryList();
874 if ($country === 'ti') {
875 $country = 'cn';
876 }
877 return isset($countryList[$country]) ? $countryList[$country] : 'unk';
878 }
879 /*
880 * Campaign
881 */
882 /**
883 * Returns the list of Campaign parameter names that will be read to classify
884 * a visit as coming from a Campaign
885 *
886 * @return array array(
887 * 0 => array( ... ) // campaign names parameters
888 * 1 => array( ... ) // campaign keyword parameters
889 * );
890 */
891 public static function getCampaignParameters()
892 {
893 $return = array(\Piwik\Config::getInstance()->Tracker['campaign_var_name'], \Piwik\Config::getInstance()->Tracker['campaign_keyword_var_name']);
894 foreach ($return as &$list) {
895 if (strpos($list, ',') !== false) {
896 $list = explode(',', $list);
897 } else {
898 $list = array($list);
899 }
900 $list = array_map('trim', $list);
901 }
902 return $return;
903 }
904 /*
905 * Referrer
906 */
907 /**
908 * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
909 * to fill the `IN (...)` part of a query.
910 *
911 * @param array|string $fields The names of the mysql table fields to bind, e.g.
912 * `array(fieldName1, fieldName2, fieldName3)`.
913 *
914 * _Note: The content of the array isn't important, just its length._
915 * @return string The placeholder string, e.g. `"?, ?, ?"`.
916 * @api
917 */
918 public static function getSqlStringFieldsArray($fields)
919 {
920 if (is_string($fields)) {
921 $fields = array($fields);
922 }
923 $count = count($fields);
924 if ($count === 0) {
925 return "''";
926 }
927 return '?' . str_repeat(',?', $count - 1);
928 }
929 /**
930 * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
931 * If for instance a German locale is used it would be a comma otherwise.
932 *
933 * @param float|string $value
934 * @return string
935 */
936 public static function forceDotAsSeparatorForDecimalPoint($value)
937 {
938 if (null === $value || false === $value) {
939 return $value;
940 }
941 return str_replace(',', '.', $value);
942 }
943 /**
944 * Sets outgoing header.
945 *
946 * @param string $header The header.
947 * @param bool $replace Whether to replace existing or not.
948 */
949 public static function sendHeader($header, $replace = true)
950 {
951 // don't send header in CLI mode
952 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
953 header($header, $replace);
954 }
955 }
956 /**
957 * Strips outgoing header.
958 *
959 * @param string $name The header name.
960 */
961 public static function stripHeader($name)
962 {
963 // don't strip header in CLI mode
964 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
965 header_remove($name);
966 }
967 }
968 /**
969 * Sends the given response code if supported.
970 *
971 * @param int $code Eg 204
972 *
973 * @throws Exception
974 */
975 public static function sendResponseCode($code)
976 {
977 $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');
978 if (!array_key_exists($code, $messages)) {
979 throw new Exception('Response code not supported: ' . $code);
980 }
981 if (strpos(PHP_SAPI, '-fcgi') === false) {
982 $key = 'HTTP/1.1';
983 if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && strlen($_SERVER['SERVER_PROTOCOL']) < 15 && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
984 $key = $_SERVER['SERVER_PROTOCOL'];
985 }
986 } else {
987 // FastCGI
988 $key = 'Status:';
989 }
990 $message = $messages[$code];
991 \Piwik\Common::sendHeader($key . ' ' . $code . ' ' . $message);
992 }
993 /**
994 * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
995 * the Tracker cache.
996 */
997 public static function getCurrentLocationProviderId()
998 {
999 $cache = TrackerCache::getCacheGeneral();
1000 return empty($cache['currentLocationProviderId']) ? \Piwik\Plugins\UserCountry\LocationProvider::getDefaultProviderId() : $cache['currentLocationProviderId'];
1001 }
1002 /**
1003 * Marks an orphaned object for garbage collection.
1004 *
1005 * For more information: {@link https://github.com/piwik/piwik/issues/374}
1006 * @param mixed $var The object to destroy.
1007 * @api
1008 */
1009 public static function destroy(&$var)
1010 {
1011 if (is_object($var) && method_exists($var, '__destruct')) {
1012 $var->__destruct();
1013 }
1014 unset($var);
1015 $var = null;
1016 }
1017 /**
1018 * @deprecated Use the logger directly instead.
1019 */
1020 public static function printDebug($info = '')
1021 {
1022 if (is_object($info)) {
1023 $info = var_export($info, true);
1024 }
1025 $logger = StaticContainer::get(LoggerInterface::class);
1026 if (is_array($info) || is_object($info)) {
1027 $out = var_export($info, true);
1028 $logger->debug($out);
1029 } else {
1030 $logger->debug($info);
1031 }
1032 }
1033 /**
1034 * Returns true if the request is an AJAX request.
1035 *
1036 * @return bool
1037 */
1038 public static function isXmlHttpRequest()
1039 {
1040 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
1041 }
1042 /**
1043 * @param $validLanguages
1044 * @return array
1045 */
1046 protected static function checkValidLanguagesIsSet($validLanguages)
1047 {
1048 /** @var LanguageDataProvider $dataProvider */
1049 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
1050 if (empty($validLanguages)) {
1051 $validLanguages = array_keys($dataProvider->getLanguageList());
1052 return $validLanguages;
1053 }
1054 return $validLanguages;
1055 }
1056 }
1057