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