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