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