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 */ 9namespace Piwik; 10 11use Exception; 12use Piwik\Container\StaticContainer; 13use Piwik\Period\Day; 14use Piwik\Period\Month; 15use Piwik\Period\Range; 16use Piwik\Period\Week; 17use Piwik\Period\Year; 18use Piwik\Plugins\UsersManager\API as APIUsersManager; 19use Piwik\Plugins\UsersManager\Model; 20use Piwik\Translation\Translator; 21 22/** 23 * Main piwik helper class. 24 * 25 * Contains helper methods for a variety of common tasks. Plugin developers are 26 * encouraged to reuse these methods as much as possible. 27 */ 28class Piwik 29{ 30 /** 31 * Piwik periods 32 * @var array 33 */ 34 public static $idPeriods = array( 35 'day' => Day::PERIOD_ID, 36 'week' => Week::PERIOD_ID, 37 'month' => Month::PERIOD_ID, 38 'year' => Year::PERIOD_ID, 39 'range' => Range::PERIOD_ID, 40 ); 41 42 /** 43 * The idGoal query parameter value for the special 'abandoned carts' goal. 44 * 45 * @api 46 */ 47 const LABEL_ID_GOAL_IS_ECOMMERCE_CART = 'ecommerceAbandonedCart'; 48 49 /** 50 * The idGoal query parameter value for the special 'ecommerce' goal. 51 * 52 * @api 53 */ 54 const LABEL_ID_GOAL_IS_ECOMMERCE_ORDER = 'ecommerceOrder'; 55 56 /** 57 * Trigger E_USER_ERROR with optional message 58 * 59 * @param string $message 60 */ 61 public static function error($message = '') 62 { 63 trigger_error($message, E_USER_ERROR); 64 } 65 66 /** 67 * Display the message in a nice red font with a nice icon 68 * ... and dies 69 * 70 * @param string $message 71 */ 72 public static function exitWithErrorMessage($message) 73 { 74 Common::sendHeader('Content-Type: text/html; charset=utf-8'); 75 76 $message = str_replace("\n", "<br/>", $message); 77 78 $output = "<html><body>". 79 "<style>a{color:red;}</style>\n" . 80 "<div style='color:red;font-size:120%; width:100%;margin: 30px;'>" . 81 " <div style='width: 50px; float: left;'><img src='plugins/Morpheus/images/error_medium.png' /></div>" . 82 " <div style='margin-left: 70px; min-width: 950px;'>" . 83 $message . 84 " </div>" . 85 " </div>" . 86 "</div>". 87 "</body></html>"; 88 print($output); 89 exit; 90 } 91 92 /** 93 * Computes the division of i1 by i2. If either i1 or i2 are not number, or if i2 has a value of zero 94 * we return 0 to avoid the division by zero. 95 * 96 * @param number $i1 97 * @param number $i2 98 * @return number The result of the division or zero 99 */ 100 public static function secureDiv($i1, $i2) 101 { 102 if (is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) { 103 return $i1 / $i2; 104 } 105 return 0; 106 } 107 108 /** 109 * Safely compute a percentage. Return 0 to avoid division by zero. 110 * 111 * @param number $dividend 112 * @param number $divisor 113 * @param int $precision 114 * @return number 115 */ 116 public static function getPercentageSafe($dividend, $divisor, $precision = 0) 117 { 118 return self::getQuotientSafe(100 * $dividend, $divisor, $precision); 119 } 120 121 /** 122 * Safely compute a ratio. Returns 0 if divisor is 0 (to avoid division by 0 error). 123 * 124 * @param number $dividend 125 * @param number $divisor 126 * @param int $precision 127 * @return number 128 */ 129 public static function getQuotientSafe($dividend, $divisor, $precision = 0) 130 { 131 if ($divisor == 0) { 132 return 0; 133 } 134 if ($dividend == 0 || $dividend === '-') { 135 $dividend = 0; 136 } 137 if (!is_numeric($dividend) || !is_numeric($divisor)) { 138 throw new \Exception(sprintf('Trying to round unsupported operands for dividend %s (%s) and divisor %s (%s)', 139 $dividend, gettype($dividend), $divisor, gettype($divisor))); 140 } 141 return round($dividend / $divisor, $precision); 142 } 143 144 /** 145 * Generate a title for image tags 146 * 147 * @return string 148 */ 149 public static function getRandomTitle() 150 { 151 static $titles = array( 152 'Web analytics', 153 'Open analytics platform', 154 'Real Time Web Analytics', 155 'Analytics', 156 'Real Time Analytics', 157 'Analytics in Real time', 158 'Analytics Platform', 159 'Data Platform', 160 ); 161 $id = abs(intval(md5(Url::getCurrentHost()))); 162 $title = $titles[$id % count($titles)]; 163 return $title; 164 } 165 166 /* 167 * Access 168 */ 169 170 /** 171 * Returns the current user's email address. 172 * 173 * @return string 174 * @api 175 */ 176 public static function getCurrentUserEmail() 177 { 178 $user = APIUsersManager::getInstance()->getUser(Piwik::getCurrentUserLogin()); 179 return $user['email'] ?? ''; 180 } 181 182 183 public static function getCurrentUserCreationDate() 184 { 185 $user = APIUsersManager::getInstance()->getUser(Piwik::getCurrentUserLogin()); 186 return $user['date_registered'] ?? ''; 187 } 188 189 /** 190 * Returns the current user's Last Seen. 191 * 192 * @return string 193 * @api 194 */ 195 public static function getCurrentUserLastSeen() 196 { 197 $user = APIUsersManager::getInstance()->getUser(Piwik::getCurrentUserLogin()); 198 return $user['last_seen'] ?? ''; 199 } 200 201 /** 202 * Returns the email addresses configured as contact. If none is configured the mail addresses of all super users will be returned instead. 203 * 204 * @return array 205 */ 206 public static function getContactEmailAddresses(): array 207 { 208 $contactAddresses = trim(Config::getInstance()->General['contact_email_address']); 209 210 if (empty($contactAddresses)) { 211 return self::getAllSuperUserAccessEmailAddresses(); 212 } 213 214 $contactAddresses = explode(',', $contactAddresses); 215 return array_map('trim', $contactAddresses); 216 } 217 218 /** 219 * Get a list of all email addresses having Super User access. 220 * 221 * @return array 222 */ 223 public static function getAllSuperUserAccessEmailAddresses() 224 { 225 $emails = array(); 226 227 try { 228 $superUsers = APIUsersManager::getInstance()->getUsersHavingSuperUserAccess(); 229 } catch (\Exception $e) { 230 return $emails; 231 } 232 233 foreach ($superUsers as $superUser) { 234 $emails[$superUser['login']] = $superUser['email']; 235 } 236 237 return $emails; 238 } 239 240 /** 241 * Returns the current user's username. 242 * 243 * @return string 244 * @api 245 */ 246 public static function getCurrentUserLogin() 247 { 248 $login = Access::getInstance()->getLogin(); 249 250 if (empty($login)) { 251 return 'anonymous'; 252 } 253 return $login; 254 } 255 256 /** 257 * Returns the current user's token auth. 258 * 259 * @return string 260 * @api 261 */ 262 public static function getCurrentUserTokenAuth() 263 { 264 return Access::getInstance()->getTokenAuth(); 265 } 266 267 /** 268 * Returns `true` if the current user is either the Super User or the user specified by 269 * `$theUser`. 270 * 271 * @param string $theUser A username. 272 * @return bool 273 * @api 274 */ 275 public static function hasUserSuperUserAccessOrIsTheUser($theUser) 276 { 277 try { 278 self::checkUserHasSuperUserAccessOrIsTheUser($theUser); 279 return true; 280 } catch (Exception $e) { 281 return false; 282 } 283 } 284 285 /** 286 * Check that the current user is either the specified user or the superuser. 287 * 288 * @param string $theUser A username. 289 * @throws NoAccessException If the user is neither the Super User nor the user `$theUser`. 290 * @api 291 */ 292 public static function checkUserHasSuperUserAccessOrIsTheUser($theUser) 293 { 294 try { 295 if (Piwik::getCurrentUserLogin() !== $theUser) { 296 // or to the Super User 297 Piwik::checkUserHasSuperUserAccess(); 298 } 299 } catch (NoAccessException $e) { 300 throw new NoAccessException(Piwik::translate('General_ExceptionCheckUserHasSuperUserAccessOrIsTheUser', array($theUser))); 301 } 302 } 303 304 /** 305 * Request a token auth to authenticate in a request. 306 * 307 * Note: During one request the token is only being requested once and used throughout the request. So you want to make 308 * sure the token is valid for enough time for the whole request to finish. 309 * 310 * @param string $reason some short string/text explaining the reason for the token generation, eg "CliMultiAsyncHttpArchiving" 311 * @param int $validForHours For how many hours the token should be valid. Should not be valid for more than 14 days. 312 * @return mixed 313 */ 314 public static function requestTemporarySystemAuthToken($reason, $validForHours) 315 { 316 static $token = array(); 317 318 if (isset($token[$reason])) { 319 // note: For now we do not increase the expire time when it is already requested 320 return $token[$reason]; 321 } 322 323 $twoWeeksInHours = 14 * 24; 324 if ($validForHours > $twoWeeksInHours) { 325 throw new Exception('The token cannot be valid for so many hours: ' . $validForHours); 326 } 327 328 $model = new Model(); 329 $users = $model->getUsersHavingSuperUserAccess(); 330 if (!empty($users)) { 331 $user = reset($users); 332 $expireDate = Date::now()->addHour($validForHours)->getDatetime(); 333 334 $token[$reason] = $model->generateRandomTokenAuth(); 335 336 $model->addTokenAuth( 337 $user['login'], 338 $token[$reason], 339 'System generated ' . $reason, 340 Date::now()->getDatetime(), 341 $expireDate, 342 true); 343 344 return $token[$reason]; 345 } 346 347 } 348 349 /** 350 * Check whether the given user has superuser access. 351 * 352 * @param string $theUser A username. 353 * @return bool 354 * @api 355 */ 356 public static function hasTheUserSuperUserAccess($theUser) 357 { 358 if (empty($theUser)) { 359 return false; 360 } 361 362 if (Piwik::getCurrentUserLogin() === $theUser && Piwik::hasUserSuperUserAccess()) { 363 return true; 364 } 365 366 try { 367 $superUsers = APIUsersManager::getInstance()->getUsersHavingSuperUserAccess(); 368 } catch (\Exception $e) { 369 return false; 370 } 371 372 foreach ($superUsers as $superUser) { 373 if ($theUser === $superUser['login']) { 374 return true; 375 } 376 } 377 378 return false; 379 } 380 381 /** 382 * Returns true if the current user has Super User access. 383 * 384 * @return bool 385 * @api 386 */ 387 public static function hasUserSuperUserAccess() 388 { 389 try { 390 $hasAccess = Access::getInstance()->hasSuperUserAccess(); 391 392 return $hasAccess; 393 } catch (Exception $e) { 394 return false; 395 } 396 } 397 398 /** 399 * Returns true if the current user is the special **anonymous** user or not. 400 * 401 * @return bool 402 * @api 403 */ 404 public static function isUserIsAnonymous() 405 { 406 $currentUserLogin = Piwik::getCurrentUserLogin(); 407 $isSuperUser = self::hasUserSuperUserAccess(); 408 return !$isSuperUser && $currentUserLogin && strtolower($currentUserLogin) == 'anonymous'; 409 } 410 411 /** 412 * Checks that the user is not the anonymous user. 413 * 414 * @throws NoAccessException if the current user is the anonymous user. 415 * @api 416 */ 417 public static function checkUserIsNotAnonymous() 418 { 419 Access::getInstance()->checkUserIsNotAnonymous(); 420 } 421 422 /** 423 * Check that the current user has superuser access. 424 * 425 * @throws Exception if the current user is not the superuser. 426 * @api 427 */ 428 public static function checkUserHasSuperUserAccess() 429 { 430 Access::getInstance()->checkUserHasSuperUserAccess(); 431 } 432 433 /** 434 * Returns `true` if the user has admin access to the requested sites, `false` if otherwise. 435 * 436 * @param int|array $idSites The list of site IDs to check access for. 437 * @return bool 438 * @api 439 */ 440 public static function isUserHasAdminAccess($idSites) 441 { 442 try { 443 self::checkUserHasAdminAccess($idSites); 444 return true; 445 } catch (Exception $e) { 446 return false; 447 } 448 } 449 450 /** 451 * Checks that the current user has admin access to the requested list of sites. 452 * 453 * @param int|array $idSites One or more site IDs to check access for. 454 * @throws Exception If user doesn't have admin access. 455 * @api 456 */ 457 public static function checkUserHasAdminAccess($idSites) 458 { 459 Access::getInstance()->checkUserHasAdminAccess($idSites); 460 } 461 462 /** 463 * Returns `true` if the current user has admin access to at least one site. 464 * 465 * @return bool 466 * @api 467 */ 468 public static function isUserHasSomeAdminAccess() 469 { 470 return Access::getInstance()->isUserHasSomeAdminAccess(); 471 } 472 473 /** 474 * Checks that the current user has write access to at least one site. 475 * 476 * @throws Exception if user doesn't have write access to any site. 477 * @api 478 */ 479 public static function checkUserHasSomeWriteAccess() 480 { 481 Access::getInstance()->checkUserHasSomeWriteAccess(); 482 } 483 484 /** 485 * Returns `true` if the current user has write access to at least one site. 486 * 487 * @return bool 488 * @api 489 */ 490 public static function isUserHasSomeWriteAccess() 491 { 492 return Access::getInstance()->isUserHasSomeWriteAccess(); 493 } 494 495 /** 496 * Checks whether the user has the given capability or not. 497 * @param array $idSites 498 * @param string $capability 499 * @throws NoAccessException Thrown if the user does not have the given capability 500 */ 501 public static function checkUserHasCapability($idSites, $capability) 502 { 503 Access::getInstance()->checkUserHasCapability($idSites, $capability); 504 } 505 506 /** 507 * Returns `true` if the current user has the given capability for the given sites. 508 * 509 * @return bool 510 * @api 511 */ 512 public static function isUserHasCapability($idSites, $capability) 513 { 514 try { 515 self::checkUserHasCapability($idSites, $capability); 516 return true; 517 } catch (Exception $e) { 518 return false; 519 } 520 } 521 522 /** 523 * Checks that the current user has admin access to at least one site. 524 * 525 * @throws Exception if user doesn't have admin access to any site. 526 * @api 527 */ 528 public static function checkUserHasSomeAdminAccess() 529 { 530 Access::getInstance()->checkUserHasSomeAdminAccess(); 531 } 532 533 /** 534 * Returns `true` if the user has view access to the requested list of sites. 535 * 536 * @param int|array $idSites One or more site IDs to check access for. 537 * @return bool 538 * @api 539 */ 540 public static function isUserHasViewAccess($idSites) 541 { 542 try { 543 self::checkUserHasViewAccess($idSites); 544 return true; 545 } catch (Exception $e) { 546 return false; 547 } 548 } 549 550 /** 551 * Returns `true` if the user has write access to the requested list of sites. 552 * 553 * @param int|array $idSites One or more site IDs to check access for. 554 * @return bool 555 * @api 556 */ 557 public static function isUserHasWriteAccess($idSites) 558 { 559 try { 560 self::checkUserHasWriteAccess($idSites); 561 return true; 562 } catch (Exception $e) { 563 return false; 564 } 565 } 566 567 /** 568 * Checks that the current user has view access to the requested list of sites 569 * 570 * @param int|array $idSites The list of site IDs to check access for. 571 * @throws Exception if the current user does not have view access to every site in the list. 572 * @api 573 */ 574 public static function checkUserHasViewAccess($idSites) 575 { 576 Access::getInstance()->checkUserHasViewAccess($idSites); 577 } 578 579 /** 580 * Checks that the current user has write access to the requested list of sites 581 * 582 * @param int|array $idSites The list of site IDs to check access for. 583 * @throws Exception if the current user does not have write access to every site in the list. 584 * @api 585 */ 586 public static function checkUserHasWriteAccess($idSites) 587 { 588 Access::getInstance()->checkUserHasWriteAccess($idSites); 589 } 590 591 /** 592 * Returns `true` if the current user has view access to at least one site. 593 * 594 * @return bool 595 * @api 596 */ 597 public static function isUserHasSomeViewAccess() 598 { 599 try { 600 self::checkUserHasSomeViewAccess(); 601 return true; 602 } catch (Exception $e) { 603 return false; 604 } 605 } 606 607 /** 608 * Checks that the current user has view access to at least one site. 609 * 610 * @throws Exception if user doesn't have view access to any site. 611 * @api 612 */ 613 public static function checkUserHasSomeViewAccess() 614 { 615 Access::getInstance()->checkUserHasSomeViewAccess(); 616 } 617 618 /* 619 * Current module, action, plugin 620 */ 621 622 /** 623 * Returns the name of the Login plugin currently being used. 624 * Must be used since it is not allowed to hardcode 'Login' in URLs 625 * in case another Login plugin is being used. 626 * 627 * @return string 628 * @api 629 */ 630 public static function getLoginPluginName() 631 { 632 return StaticContainer::get('Piwik\Auth')->getName(); 633 } 634 635 /** 636 * Returns the plugin currently being used to display the page 637 * 638 * @return Plugin 639 */ 640 public static function getCurrentPlugin() 641 { 642 return \Piwik\Plugin\Manager::getInstance()->getLoadedPlugin(Piwik::getModule()); 643 } 644 645 /** 646 * Returns the current module read from the URL (eg. 'API', 'DevicesDetection', etc.) 647 * 648 * @return string 649 */ 650 public static function getModule() 651 { 652 return Common::getRequestVar('module', '', 'string'); 653 } 654 655 /** 656 * Returns the current action read from the URL 657 * 658 * @return string 659 */ 660 public static function getAction() 661 { 662 return Common::getRequestVar('action', '', 'string'); 663 } 664 665 /** 666 * Helper method used in API function to introduce array elements in API parameters. 667 * Array elements can be passed by comma separated values, or using the notation 668 * array[]=value1&array[]=value2 in the URL. 669 * This function will handle both cases and return the array. 670 * 671 * @param array|string $columns 672 * @return array 673 */ 674 public static function getArrayFromApiParameter($columns, $unique = true) 675 { 676 if (empty($columns)) { 677 return array(); 678 } 679 if (is_array($columns)) { 680 return $columns; 681 } 682 $array = explode(',', $columns); 683 if ($unique) { 684 $array = array_unique($array); 685 } 686 return $array; 687 } 688 689 /** 690 * Redirects the current request to a new module and action. 691 * 692 * @param string $newModule The target module, eg, `'UserCountry'`. 693 * @param string $newAction The target controller action, eg, `'index'`. 694 * @param array $parameters The query parameter values to modify before redirecting. 695 * @api 696 */ 697 public static function redirectToModule($newModule, $newAction = '', $parameters = array()) 698 { 699 $newUrl = 'index.php' . Url::getCurrentQueryStringWithParametersModified( 700 array('module' => $newModule, 'action' => $newAction) 701 + $parameters 702 ); 703 Url::redirectToUrl($newUrl); 704 } 705 706 /* 707 * User input validation 708 */ 709 710 /** 711 * Returns `true` if supplied the email address is a valid. 712 * 713 * @param string $emailAddress 714 * @return bool 715 * @api 716 */ 717 public static function isValidEmailString($emailAddress) 718 { 719 return filter_var($emailAddress, FILTER_VALIDATE_EMAIL) !== false; 720 } 721 722 /** 723 * Returns `true` if the login is valid. 724 * 725 * _Warning: does not check if the login already exists! You must use UsersManager_API->userExists as well._ 726 * 727 * @param string $userLogin 728 * @throws Exception 729 * @return bool 730 */ 731 public static function checkValidLoginString($userLogin) 732 { 733 if (!SettingsPiwik::isUserCredentialsSanityCheckEnabled() 734 && !empty($userLogin) 735 ) { 736 return; 737 } 738 $loginMinimumLength = 2; 739 $loginMaximumLength = 100; 740 $l = strlen($userLogin); 741 if (!($l >= $loginMinimumLength 742 && $l <= $loginMaximumLength 743 && (preg_match('/^[A-Za-zÄäÖöÜüß0-9_.@+-]*$/D', $userLogin) > 0)) 744 ) { 745 throw new Exception(Piwik::translate('UsersManager_ExceptionInvalidLoginFormat', array($loginMinimumLength, $loginMaximumLength))); 746 } 747 } 748 749 /** 750 * Utility function that checks if an object type is in a set of types. 751 * 752 * @param mixed $o 753 * @param array $types List of class names that $o is expected to be one of. 754 * @throws Exception if $o is not an instance of the types contained in $types. 755 */ 756 public static function checkObjectTypeIs($o, $types) 757 { 758 foreach ($types as $type) { 759 if ($o instanceof $type) { 760 return; 761 } 762 } 763 764 $oType = is_object($o) ? get_class($o) : gettype($o); 765 throw new Exception("Invalid variable type '$oType', expected one of following: " . implode(', ', $types)); 766 } 767 768 /** 769 * Returns true if an array is an associative array, false if otherwise. 770 * 771 * This method determines if an array is associative by checking that the 772 * first element's key is 0, and that each successive element's key is 773 * one greater than the last. 774 * 775 * @param array $array 776 * @return bool 777 */ 778 public static function isAssociativeArray($array) 779 { 780 reset($array); 781 if (!is_numeric(key($array)) 782 || key($array) != 0 783 ) { 784 // first key must be 0 785 786 return true; 787 } 788 789 // check that each key is == next key - 1 w/o actually indexing the array 790 while (true) { 791 $current = key($array); 792 793 next($array); 794 $next = key($array); 795 796 if ($next === null) { 797 break; 798 } elseif ($current + 1 != $next) { 799 return true; 800 } 801 } 802 803 return false; 804 } 805 806 public static function isMultiDimensionalArray($array) 807 { 808 $first = reset($array); 809 foreach ($array as $first) { 810 if (is_array($first)) { 811 // Yes, this is a multi dim array 812 return true; 813 } 814 } 815 816 return false; 817 } 818 819 /** 820 * Returns the class name of an object without its namespace. 821 * 822 * @param mixed|string $object 823 * @return string 824 */ 825 public static function getUnnamespacedClassName($object) 826 { 827 $className = is_string($object) ? $object : get_class($object); 828 $parts = explode('\\', $className); 829 return end($parts); 830 } 831 832 /** 833 * Post an event to Piwik's event dispatcher which will execute the event's observers. 834 * 835 * @param string $eventName The event name. 836 * @param array $params The parameter array to forward to observer callbacks. 837 * @param bool $pending If true, plugins that are loaded after this event is fired will 838 * have their observers for this event executed. 839 * @param array|null $plugins The list of plugins to execute observers for. If null, all 840 * plugin observers will be executed. 841 * @api 842 */ 843 public static function postEvent($eventName, $params = array(), $pending = false, $plugins = null) 844 { 845 EventDispatcher::getInstance()->postEvent($eventName, $params, $pending, $plugins); 846 } 847 848 /** 849 * Register an observer to an event. 850 * 851 * **_Note: Observers should normally be defined in plugin objects. It is unlikely that you will 852 * need to use this function._** 853 * 854 * @param string $eventName The event name. 855 * @param callable|array $function The observer. 856 * @api 857 */ 858 public static function addAction($eventName, $function) 859 { 860 EventDispatcher::getInstance()->addObserver($eventName, $function); 861 } 862 863 /** 864 * Posts an event if we are currently running tests. Whether we are running tests is 865 * determined by looking for the PIWIK_TEST_MODE constant. 866 */ 867 public static function postTestEvent($eventName, $params = array(), $pending = false, $plugins = null) 868 { 869 if (defined('PIWIK_TEST_MODE')) { 870 Piwik::postEvent($eventName, $params, $pending, $plugins); 871 } 872 } 873 874 /** 875 * Returns an internationalized string using a translation token. If a translation 876 * cannot be found for the token, the token is returned. 877 * 878 * @param string $translationId Translation ID, eg, `'General_Date'`. 879 * @param array|string|int $args `sprintf` arguments to be applied to the internationalized 880 * string. 881 * @param string|null $language Optionally force the language. 882 * @return string The translated string or `$translationId`. 883 * @api 884 */ 885 public static function translate($translationId, $args = array(), $language = null) 886 { 887 /** @var Translator $translator */ 888 $translator = StaticContainer::get('Piwik\Translation\Translator'); 889 890 return $translator->translate($translationId, $args, $language); 891 } 892} 893