PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.1
5.13.0 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
1056 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 settype($varDefault, $varType);
510 return $varDefault;
511 }
512 }
513 settype($value, $varType);
514 }
515 return $value;
516 }
517 /*
518 * Generating unique strings
519 */
520 /**
521 * Generates a random integer
522 *
523 * @param int $min
524 * @param null|int $max Defaults to max int value
525 * @return int
526 */
527 public static function getRandomInt($min = 0, $max = null)
528 {
529 if (!isset($max)) {
530 $max = PHP_INT_MAX;
531 }
532 return random_int($min, $max);
533 }
534 /**
535 * Returns a 32 characters long uniq ID
536 *
537 * @return string 32 chars
538 */
539 public static function generateUniqId()
540 {
541 return bin2hex(random_bytes(16));
542 }
543 /**
544 * Configurable hash() algorithm (defaults to md5)
545 *
546 * @param string $str String to be hashed
547 * @param bool $raw_output
548 * @return string Hash string
549 */
550 public static function hash($str, $raw_output = false)
551 {
552 static $hashAlgorithm = null;
553 if (is_null($hashAlgorithm)) {
554 $hashAlgorithm = @\Piwik\Config::getInstance()->General['hash_algorithm'];
555 }
556 if ($hashAlgorithm) {
557 $hash = @hash($hashAlgorithm, $str, $raw_output);
558 if ($hash !== false) {
559 return $hash;
560 }
561 }
562 return md5($str, $raw_output);
563 }
564 /**
565 * Generate random string.
566 *
567 * @param int $length string length
568 * @param string $alphabet characters allowed in random string
569 * @return string random string with given length
570 */
571 public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
572 {
573 $chars = $alphabet;
574 $str = '';
575 for ($i = 0; $i < $length; $i++) {
576 $rand_key = self::getRandomInt(0, strlen($chars) - 1);
577 $str .= substr($chars, $rand_key, 1);
578 }
579 return str_shuffle($str);
580 }
581 /*
582 * Conversions
583 */
584 /**
585 * Convert hexadecimal representation into binary data.
586 * !! Will emit warning if input string is not hex!!
587 *
588 * @see http://php.net/bin2hex
589 *
590 * @param string $str Hexadecimal representation
591 * @return string
592 */
593 public static function hex2bin($str)
594 {
595 return pack("H*", $str);
596 }
597 /**
598 * This function will convert the input string to the binary representation of the ID
599 * but it will throw an Exception if the specified input ID is not correct
600 *
601 * This is used when building segments containing visitorId which could be an invalid string
602 * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
603 *
604 * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
605 * so better be safe and get the php error when something unexpected is happening
606 * @param string $id
607 * @throws Exception
608 * @return string binary string
609 */
610 public static function convertVisitorIdToBin($id)
611 {
612 if (strlen($id) !== \Piwik\Tracker::LENGTH_HEX_ID_STRING || @bin2hex(self::hex2bin($id)) != $id) {
613 throw new Exception("visitorId is expected to be a " . \Piwik\Tracker::LENGTH_HEX_ID_STRING . " hex char string");
614 }
615 return self::hex2bin($id);
616 }
617 /**
618 * Converts a User ID string to the Visitor ID Binary representation.
619 *
620 * @param $userId
621 * @return string
622 */
623 public static function convertUserIdToVisitorIdBin($userId)
624 {
625 $userIdHashed = \MatomoTracker::getUserIdHashed($userId);
626 return self::convertVisitorIdToBin($userIdHashed);
627 }
628 /**
629 * Detects whether an error occurred during the last json encode/decode.
630 * @return bool
631 */
632 public static function hasJsonErrorOccurred()
633 {
634 return json_last_error() != JSON_ERROR_NONE;
635 }
636 /**
637 * Returns a human readable error message in case an error occurred during the last json encode/decode.
638 * Returns an empty string in case there was no error.
639 *
640 * @return string
641 */
642 public static function getLastJsonError()
643 {
644 switch (json_last_error()) {
645 case JSON_ERROR_NONE:
646 return '';
647 case JSON_ERROR_DEPTH:
648 return 'Maximum stack depth exceeded';
649 case JSON_ERROR_STATE_MISMATCH:
650 return 'Underflow or the modes mismatch';
651 case JSON_ERROR_CTRL_CHAR:
652 return 'Unexpected control character found';
653 case JSON_ERROR_SYNTAX:
654 return 'Syntax error, malformed JSON';
655 case JSON_ERROR_UTF8:
656 return 'Malformed UTF-8 characters, possibly incorrectly encoded';
657 }
658 return 'Unknown error';
659 }
660 public static function stringEndsWith($haystack, $needle)
661 {
662 if (strlen(strval($needle)) === 0) {
663 return true;
664 }
665 if (strlen(strval($haystack)) === 0) {
666 return false;
667 }
668 $lastCharacters = substr($haystack, -strlen($needle));
669 return $lastCharacters === $needle;
670 }
671 /**
672 * Returns the list of parent classes for the given class.
673 *
674 * @param string $class A class name.
675 * @return string[] The list of parent classes in order from highest ancestor to the descended class.
676 */
677 public static function getClassLineage($class)
678 {
679 $classes = array_merge(array($class), array_values(class_parents($class, $autoload = false)));
680 return array_reverse($classes);
681 }
682 /*
683 * DataFiles
684 */
685 /**
686 * Returns list of provider names
687 *
688 * @see core/DataFiles/Providers.php
689 *
690 * @return array Array of ( dnsName => providerName )
691 */
692 public static function getProviderNames()
693 {
694 require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
695 $providers = $GLOBALS['Piwik_ProviderNames'];
696 return $providers;
697 }
698 /*
699 * Language, country, continent
700 */
701 /**
702 * Returns the browser language code, eg. "en-gb,en;q=0.5"
703 *
704 * @param string|null $browserLang Optional browser language, otherwise taken from the request header
705 * @return string
706 */
707 public static function getBrowserLanguage($browserLang = null)
708 {
709 static $replacementPatterns = array(
710 // extraneous bits of RFC 3282 that we ignore
711 '/(\\\\.)/',
712 // quoted-pairs
713 '/(\\s+)/',
714 // CFWcS white space
715 '/(\\([^)]*\\))/',
716 // CFWS comments
717 '/(;q=[0-9.]+)/',
718 // quality
719 // found in the LANG environment variable
720 '/\\.(.*)/',
721 // charset (e.g., en_CA.UTF-8)
722 '/^C$/',
723 );
724 if (is_null($browserLang)) {
725 $browserLang = self::sanitizeInputValues($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
726 if (empty($browserLang) && self::isPhpCliMode()) {
727 $browserLang = @getenv('LANG');
728 }
729 }
730 if (empty($browserLang)) {
731 // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
732 $browserLang = "";
733 } else {
734 // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
735 // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
736 // but we guard against a bad user agent which naively uses its locale
737 $browserLang = strtolower(str_replace('_', '-', $browserLang));
738 // filters
739 $browserLang = preg_replace($replacementPatterns, '', $browserLang);
740 $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1);
741 // Firefox bug
742 $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1);
743 // unregistered language tag
744 $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang);
745 // unofficial (proposed) code in the wild
746 }
747 return $browserLang;
748 }
749 /**
750 * Returns the visitor country based on the Browser 'accepted language'
751 * information, but provides a hook for geolocation via IP address.
752 *
753 * @param string $lang browser lang
754 * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
755 * @param string $ip
756 * @return string 2 letter ISO code
757 */
758 public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
759 {
760 if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
761 return self::LANGUAGE_CODE_INVALID;
762 }
763 /** @var RegionDataProvider $dataProvider */
764 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
765 $validCountries = $dataProvider->getCountryList();
766 return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
767 }
768 /**
769 * Returns list of valid country codes
770 *
771 * @param string $browserLanguage
772 * @param array $validCountries Array of valid countries
773 * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
774 * @return array Array of 2 letter ISO codes
775 */
776 public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
777 {
778 /** @var LanguageDataProvider $dataProvider */
779 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
780 $langToCountry = $dataProvider->getLanguageToCountryList();
781 if ($enableLanguageToCountryGuess) {
782 if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
783 // match language (without region) to infer the country of origin
784 if (array_key_exists($matches[1], $langToCountry)) {
785 return $langToCountry[$matches[1]];
786 }
787 }
788 }
789 if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
790 foreach ($matches as $parts) {
791 // match location; we don't make any inferences from the language
792 if (array_key_exists($parts[1], $validCountries)) {
793 return $parts[1];
794 }
795 }
796 }
797 return self::LANGUAGE_CODE_INVALID;
798 }
799 /**
800 * Returns the language string, based only on the Browser 'accepted language' information.
801 * * The language tag is defined by ISO 639-1
802 *
803 * @param string $browserLanguage Browser's accepted language header
804 * @param array $validLanguages array of valid language codes
805 * @return string 2 letter ISO 639 code 'es' (Spanish)
806 */
807 public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
808 {
809 $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
810 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
811 if (strlen($languageRegionCode) === 2) {
812 $languageCode = $languageRegionCode;
813 } else {
814 $languageCode = substr($languageRegionCode, 0, 2);
815 }
816 if (in_array($languageCode, $validLanguages)) {
817 return $languageCode;
818 }
819 return self::LANGUAGE_CODE_INVALID;
820 }
821 /**
822 * Returns the language and region string, based only on the Browser 'accepted language' information.
823 * * The language tag is defined by ISO 639-1
824 * * The region tag is defined by ISO 3166-1
825 *
826 * @param string $browserLanguage Browser's accepted language header
827 * @param array $validLanguages array of valid language/region codes.
828 * @return string 2-letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
829 */
830 public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
831 {
832 $forceRegionValidation = !empty($validLanguages);
833 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
834 if (!preg_match_all('/(?:^|,)([a-z]{2,3})(?:[-][a-z]{4})?([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
835 return self::LANGUAGE_CODE_INVALID;
836 }
837 foreach ($matches as $parts) {
838 $langIso639 = $parts[1];
839 if (empty($langIso639)) {
840 continue;
841 }
842 // If a region tag is found eg. "fr-ca"
843 if (count($parts) === 3) {
844 $regionIso3166 = $parts[2];
845 // eg. "-ca"
846 if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
847 return $langIso639 . $regionIso3166;
848 }
849 // if a set of valid codes was provided, we do not append the region if it was not included
850 if (in_array($langIso639, $validLanguages) && !$forceRegionValidation) {
851 return $langIso639 . $regionIso3166;
852 }
853 }
854 // eg. "fr" or "es"
855 if (in_array($langIso639, $validLanguages)) {
856 return $langIso639;
857 }
858 }
859 return self::LANGUAGE_CODE_INVALID;
860 }
861 /**
862 * Returns the continent of a given country
863 *
864 * @param string $country 2 letters iso code
865 *
866 * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
867 */
868 public static function getContinent($country)
869 {
870 /** @var RegionDataProvider $dataProvider */
871 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\RegionDataProvider');
872 $countryList = $dataProvider->getCountryList();
873 if ($country === 'ti') {
874 $country = 'cn';
875 }
876 return isset($countryList[$country]) ? $countryList[$country] : 'unk';
877 }
878 /*
879 * Campaign
880 */
881 /**
882 * Returns the list of Campaign parameter names that will be read to classify
883 * a visit as coming from a Campaign
884 *
885 * @return array array(
886 * 0 => array( ... ) // campaign names parameters
887 * 1 => array( ... ) // campaign keyword parameters
888 * );
889 */
890 public static function getCampaignParameters()
891 {
892 $return = array(\Piwik\Config::getInstance()->Tracker['campaign_var_name'], \Piwik\Config::getInstance()->Tracker['campaign_keyword_var_name']);
893 foreach ($return as &$list) {
894 if (strpos($list, ',') !== false) {
895 $list = explode(',', $list);
896 } else {
897 $list = array($list);
898 }
899 $list = array_map('trim', $list);
900 }
901 return $return;
902 }
903 /*
904 * Referrer
905 */
906 /**
907 * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
908 * to fill the `IN (...)` part of a query.
909 *
910 * @param array|string $fields The names of the mysql table fields to bind, e.g.
911 * `array(fieldName1, fieldName2, fieldName3)`.
912 *
913 * _Note: The content of the array isn't important, just its length._
914 * @return string The placeholder string, e.g. `"?, ?, ?"`.
915 * @api
916 */
917 public static function getSqlStringFieldsArray($fields)
918 {
919 if (is_string($fields)) {
920 $fields = array($fields);
921 }
922 $count = count($fields);
923 if ($count === 0) {
924 return "''";
925 }
926 return '?' . str_repeat(',?', $count - 1);
927 }
928 /**
929 * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
930 * If for instance a German locale is used it would be a comma otherwise.
931 *
932 * @param float|string $value
933 * @return string
934 */
935 public static function forceDotAsSeparatorForDecimalPoint($value)
936 {
937 if (null === $value || false === $value) {
938 return $value;
939 }
940 return str_replace(',', '.', $value);
941 }
942 /**
943 * Sets outgoing header.
944 *
945 * @param string $header The header.
946 * @param bool $replace Whether to replace existing or not.
947 */
948 public static function sendHeader($header, $replace = true)
949 {
950 // don't send header in CLI mode
951 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
952 header($header, $replace);
953 }
954 }
955 /**
956 * Strips outgoing header.
957 *
958 * @param string $name The header name.
959 */
960 public static function stripHeader($name)
961 {
962 // don't strip header in CLI mode
963 if (!\Piwik\Common::isPhpCliMode() and !headers_sent()) {
964 header_remove($name);
965 }
966 }
967 /**
968 * Sends the given response code if supported.
969 *
970 * @param int $code Eg 204
971 *
972 * @throws Exception
973 */
974 public static function sendResponseCode($code)
975 {
976 $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');
977 if (!array_key_exists($code, $messages)) {
978 throw new Exception('Response code not supported: ' . $code);
979 }
980 if (strpos(PHP_SAPI, '-fcgi') === false) {
981 $key = 'HTTP/1.1';
982 if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && strlen($_SERVER['SERVER_PROTOCOL']) < 15 && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
983 $key = $_SERVER['SERVER_PROTOCOL'];
984 }
985 } else {
986 // FastCGI
987 $key = 'Status:';
988 }
989 $message = $messages[$code];
990 \Piwik\Common::sendHeader($key . ' ' . $code . ' ' . $message);
991 }
992 /**
993 * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
994 * the Tracker cache.
995 */
996 public static function getCurrentLocationProviderId()
997 {
998 $cache = TrackerCache::getCacheGeneral();
999 return empty($cache['currentLocationProviderId']) ? \Piwik\Plugins\UserCountry\LocationProvider::getDefaultProviderId() : $cache['currentLocationProviderId'];
1000 }
1001 /**
1002 * Marks an orphaned object for garbage collection.
1003 *
1004 * For more information: {@link https://github.com/piwik/piwik/issues/374}
1005 * @param mixed $var The object to destroy.
1006 * @api
1007 */
1008 public static function destroy(&$var)
1009 {
1010 if (is_object($var) && method_exists($var, '__destruct')) {
1011 $var->__destruct();
1012 }
1013 unset($var);
1014 $var = null;
1015 }
1016 /**
1017 * @deprecated Use the logger directly instead.
1018 */
1019 public static function printDebug($info = '')
1020 {
1021 if (is_object($info)) {
1022 $info = var_export($info, true);
1023 }
1024 $logger = StaticContainer::get(LoggerInterface::class);
1025 if (is_array($info) || is_object($info)) {
1026 $out = var_export($info, true);
1027 $logger->debug($out);
1028 } else {
1029 $logger->debug($info);
1030 }
1031 }
1032 /**
1033 * Returns true if the request is an AJAX request.
1034 *
1035 * @return bool
1036 */
1037 public static function isXmlHttpRequest()
1038 {
1039 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
1040 }
1041 /**
1042 * @param $validLanguages
1043 * @return array
1044 */
1045 protected static function checkValidLanguagesIsSet($validLanguages)
1046 {
1047 /** @var LanguageDataProvider $dataProvider */
1048 $dataProvider = StaticContainer::get('Piwik\\Intl\\Data\\Provider\\LanguageDataProvider');
1049 if (empty($validLanguages)) {
1050 $validLanguages = array_keys($dataProvider->getLanguageList());
1051 return $validLanguages;
1052 }
1053 return $validLanguages;
1054 }
1055 }
1056