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\Plugins\UsersManager;
10
11use DeviceDetector\DeviceDetector;
12use Exception;
13use Piwik\Access;
14use Piwik\Access\CapabilitiesProvider;
15use Piwik\Access\RolesProvider;
16use Piwik\Auth\Password;
17use Piwik\Common;
18use Piwik\Config;
19use Piwik\Container\StaticContainer;
20use Piwik\Date;
21use Piwik\IP;
22use Piwik\Mail;
23use Piwik\Metrics\Formatter;
24use Piwik\NoAccessException;
25use Piwik\Option;
26use Piwik\Piwik;
27use Piwik\Plugin;
28use Piwik\Plugins\CoreAdminHome\Emails\UserCreatedEmail;
29use Piwik\Plugins\Login\PasswordVerifier;
30use Piwik\SettingsPiwik;
31use Piwik\Site;
32use Piwik\Tracker\Cache;
33use Piwik\View;
34use Piwik\Plugins\CoreAdminHome\Emails\UserDeletedEmail;
35
36/**
37 * The UsersManager API lets you Manage Users and their permissions to access specific websites.
38 *
39 * You can create users via "addUser", update existing users via "updateUser" and delete users via "deleteUser".
40 * There are many ways to list users based on their login "getUser" and "getUsers", their email "getUserByEmail",
41 * or which users have permission (view or admin) to access the specified websites "getUsersWithSiteAccess".
42 *
43 * Existing Permissions are listed given a login via "getSitesAccessFromUser", or a website ID via "getUsersAccessFromSite",
44 * or you can list all users and websites for a given permission via "getUsersSitesFromAccess". Permissions are set and updated
45 * via the method "setUserAccess".
46 * See also the documentation about <a href='http://matomo.org/docs/manage-users/' rel='noreferrer' target='_blank'>Managing Users</a> in Matomo.
47 */
48class API extends \Piwik\Plugin\API
49{
50    const OPTION_NAME_PREFERENCE_SEPARATOR = '_';
51
52    public static $UPDATE_USER_REQUIRE_PASSWORD_CONFIRMATION = true;
53    public static $SET_SUPERUSER_ACCESS_REQUIRE_PASSWORD_CONFIRMATION = true;
54
55    /**
56     * @var Model
57     */
58    private $model;
59
60    /**
61     * @var Password
62     */
63    private $password;
64
65    /**
66     * @var UserAccessFilter
67     */
68    private $userFilter;
69
70    /**
71     * @var Access
72     */
73    private $access;
74
75    /**
76     * @var Access\RolesProvider
77     */
78    private $roleProvider;
79
80    /**
81     * @var Access\CapabilitiesProvider
82     */
83    private $capabilityProvider;
84
85    /**
86     * @var PasswordVerifier
87     */
88    private $passwordVerifier;
89
90    private $twoFaPluginActivated;
91
92    const PREFERENCE_DEFAULT_REPORT = 'defaultReport';
93    const PREFERENCE_DEFAULT_REPORT_DATE = 'defaultReportDate';
94
95    private static $instance = null;
96
97    public function __construct(Model $model, UserAccessFilter $filter, Password $password, Access $access = null, Access\RolesProvider $roleProvider = null, Access\CapabilitiesProvider $capabilityProvider = null, PasswordVerifier $passwordVerifier = null)
98    {
99        $this->model = $model;
100        $this->userFilter = $filter;
101        $this->password = $password;
102        $this->access = $access ?: StaticContainer::get(Access::class);
103        $this->roleProvider = $roleProvider ?: StaticContainer::get(RolesProvider::class);
104        $this->capabilityProvider = $capabilityProvider ?: StaticContainer::get(CapabilitiesProvider::class);
105        $this->passwordVerifier = $passwordVerifier ?: StaticContainer::get(PasswordVerifier::class);
106    }
107
108    /**
109     * You can create your own Users Plugin to override this class.
110     * Example of how you would overwrite the UsersManager_API with your own class:
111     * Call the following in your plugin __construct() for example:
112     *
113     * StaticContainer::getContainer()->set('UsersManager_API', \Piwik\Plugins\MyCustomUsersManager\API::getInstance());
114     *
115     * @throws Exception
116     * @return \Piwik\Plugins\UsersManager\API
117     */
118    public static function getInstance()
119    {
120        try {
121            $instance = StaticContainer::get('UsersManager_API');
122            if (!($instance instanceof API)) {
123                // Exception is caught below and corrected
124                throw new Exception('UsersManager_API must inherit API');
125            }
126            self::$instance = $instance;
127
128        } catch (Exception $e) {
129            self::$instance = StaticContainer::get('Piwik\Plugins\UsersManager\API');
130            StaticContainer::getContainer()->set('UsersManager_API', self::$instance);
131        }
132
133        return self::$instance;
134    }
135
136    /**
137     * Get the list of all available roles.
138     * It does not return the super user role, and neither the "noaccess" role.
139     * @return array[]  Returns an array containing information about each role
140     */
141    public function getAvailableRoles()
142    {
143        Piwik::checkUserHasSomeAdminAccess();
144
145        $response = array();
146
147        foreach ($this->roleProvider->getAllRoles() as $role) {
148            $response[] = array(
149                'id' => $role->getId(),
150                'name' => $role->getName(),
151                'description' => $role->getDescription(),
152                'helpUrl' => $role->getHelpUrl(),
153            );
154        }
155
156        return $response;
157    }
158
159    /**
160     * Get the list of all available capabilities.
161     * @return array[]  Returns an array containing information about each capability
162     */
163    public function getAvailableCapabilities()
164    {
165        Piwik::checkUserHasSomeAdminAccess();
166
167        $response = array();
168
169        foreach ($this->capabilityProvider->getAllCapabilities() as $capability) {
170            $response[] = array(
171                'id' => $capability->getId(),
172                'name' => $capability->getName(),
173                'description' => $capability->getDescription(),
174                'helpUrl' => $capability->getHelpUrl(),
175                'includedInRoles' => $capability->getIncludedInRoles(),
176                'category' => $capability->getCategory(),
177            );
178        }
179
180        return $response;
181    }
182
183    /**
184     * Sets a user preference. Plugins can add custom preference names by declaring them in their plugin config/config.php
185     * like this:
186     *
187     * ```php
188     * return array('usersmanager.user_preference_names' => DI\add(array('preference_name_1', 'preference_name_2')));
189     * ```
190     *
191     * @param string $userLogin
192     * @param string $preferenceName
193     * @param string $preferenceValue
194     * @return void
195     */
196    public function setUserPreference($userLogin, $preferenceName, $preferenceValue)
197    {
198        Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
199
200        if (!$this->model->userExists($userLogin)) {
201            throw new Exception('User does not exist: ' . $userLogin);
202        }
203
204        if ($userLogin === 'anonymous') {
205            Piwik::checkUserHasSuperUserAccess();
206        }
207
208        $nameIfSupported = $this->getPreferenceId($userLogin, $preferenceName);
209        Option::set($nameIfSupported, $preferenceValue);
210    }
211
212    /**
213     * Gets a user preference
214     * @param string $preferenceName
215     * @param string|bool $userLogin Optional, defaults to current user log in when set to false.
216     * @return bool|string
217     */
218    public function getUserPreference($preferenceName, $userLogin = false)
219    {
220        if ($userLogin === false) {
221            // the default value for first parameter is there to have it an optional parameter in the HTTP API
222            // in PHP it won't be optional. Could move parameter to the end of the method but did not want to break
223            // BC
224            $userLogin = Piwik::getCurrentUserLogin();
225        }
226        Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
227
228        $optionValue = $this->getPreferenceValue($userLogin, $preferenceName);
229
230        if ($optionValue !== false) {
231            return $optionValue;
232        }
233
234        return $this->getDefaultUserPreference($preferenceName, $userLogin);
235    }
236
237    /**
238     * Sets a user preference in the DB using the preference's default value.
239     * @param string $userLogin
240     * @param string $preferenceName
241     * @ignore
242     */
243    public function initUserPreferenceWithDefault($userLogin, $preferenceName)
244    {
245        Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
246
247        $optionValue = $this->getPreferenceValue($userLogin, $preferenceName);
248
249        if ($optionValue === false) {
250            $defaultValue = $this->getDefaultUserPreference($preferenceName, $userLogin);
251
252            if ($defaultValue !== false) {
253                $this->setUserPreference($userLogin, $preferenceName, $defaultValue);
254            }
255        }
256    }
257
258    /**
259     * Returns an array of Preferences
260     * @param $preferenceNames array of preference names
261     * @return array
262     * @ignore
263     */
264    public function getAllUsersPreferences(array $preferenceNames)
265    {
266        Piwik::checkUserHasSuperUserAccess();
267
268        $userPreferences = array();
269        foreach($preferenceNames as $preferenceName) {
270            $optionNameMatchAllUsers = $this->getPreferenceId('%', $preferenceName);
271            $preferences = Option::getLike($optionNameMatchAllUsers);
272
273            foreach($preferences as $optionName => $optionValue) {
274                $lastUnderscore = strrpos($optionName, self::OPTION_NAME_PREFERENCE_SEPARATOR);
275                $userName = substr($optionName, 0, $lastUnderscore);
276                $preference = substr($optionName, $lastUnderscore + 1);
277                $userPreferences[$userName][$preference] = $optionValue;
278            }
279        }
280        return $userPreferences;
281    }
282
283    private function getPreferenceId($login, $preference)
284    {
285        if(false !== strpos($preference, self::OPTION_NAME_PREFERENCE_SEPARATOR)) {
286            throw new Exception("Preference name cannot contain underscores.");
287        }
288        $names = array(
289            self::PREFERENCE_DEFAULT_REPORT,
290            self::PREFERENCE_DEFAULT_REPORT_DATE,
291            'isLDAPUser', // used in loginldap
292            'hideSegmentDefinitionChangeMessage',// used in JS
293        );
294        $customPreferences = StaticContainer::get('usersmanager.user_preference_names');
295
296        if (!in_array($preference, $names, true)
297            && !in_array($preference, $customPreferences, true)) {
298            throw new Exception('Not supported preference name: ' . $preference);
299        }
300        return $login . self::OPTION_NAME_PREFERENCE_SEPARATOR . $preference;
301    }
302
303    private function getPreferenceValue($userLogin, $preferenceName)
304    {
305        return Option::get($this->getPreferenceId($userLogin, $preferenceName));
306    }
307
308    private function getDefaultUserPreference($preferenceName, $login)
309    {
310        switch ($preferenceName) {
311            case self::PREFERENCE_DEFAULT_REPORT:
312                $viewableSiteIds = \Piwik\Plugins\SitesManager\API::getInstance()->getSitesIdWithAtLeastViewAccess($login);
313                if (!empty($viewableSiteIds)) {
314                    return reset($viewableSiteIds);
315                }
316                return false;
317            case self::PREFERENCE_DEFAULT_REPORT_DATE:
318                return Config::getInstance()->General['default_day'];
319            default:
320                return false;
321        }
322    }
323
324    /**
325     * Returns all users with their role for $idSite.
326     *
327     * @param int $idSite
328     * @param int|null $limit
329     * @param int|null $offset
330     * @param string|null $filter_search text to search for in the user's login and email (if any)
331     * @param string|null $filter_access only select users with this access to $idSite. can be 'noaccess', 'some', 'view', 'admin', 'superuser'
332     *                                   Filtering by 'superuser' is only allowed for other superusers.
333     * @return array
334     */
335    public function getUsersPlusRole($idSite, $limit = null, $offset = 0, $filter_search = null, $filter_access = null)
336    {
337        if (!$this->isUserHasAdminAccessTo($idSite)) {
338            // if the user is not an admin to $idSite, they can only see their own user
339            if ($offset > 1) {
340                Common::sendHeader('X-Matomo-Total-Results: 1');
341                return [];
342            }
343
344            $user = $this->model->getUser($this->access->getLogin());
345            $user['role'] = $this->access->getRoleForSite($idSite);
346            $user['capabilities'] = $this->access->getCapabilitiesForSite($idSite);
347            $users = [$user];
348            $totalResults = 1;
349        } else {
350            // if the current user is not the superuser, only select users that have access to a site this user
351            // has admin access to
352            $loginsToLimit = null;
353            if (!Piwik::hasUserSuperUserAccess()) {
354                $adminIdSites = Access::getInstance()->getSitesIdWithAdminAccess();
355                if (empty($adminIdSites)) { // sanity check
356                    throw new \Exception("The current admin user does not have access to any sites.");
357                }
358
359                $loginsToLimit = $this->model->getUsersWithAccessToSites($adminIdSites);
360            }
361
362            if ($loginsToLimit !== null && empty($loginsToLimit)) {
363                // if the current user is not the superuser, and getUsersWithAccessToSites() returned an empty result,
364                // access is managed by another plugin, and the current user cannot manage any user with UsersManager
365                Common::sendHeader('X-Matomo-Total-Results: 0');
366                return [];
367
368            } else {
369                [$users, $totalResults] = $this->model->getUsersWithRole($idSite, $limit, $offset, $filter_search, $filter_access, $loginsToLimit);
370
371                foreach ($users as &$user) {
372                    $user['superuser_access'] = $user['superuser_access'] == 1;
373                    if ($user['superuser_access']) {
374                        $user['role'] = 'superuser';
375                        $user['capabilities'] = [];
376                    } else {
377                        [$user['role'], $user['capabilities']] = $this->getRoleAndCapabilitiesFromAccess($user['access']);
378                        $user['role'] = empty($user['role']) ? 'noaccess' : reset($user['role']);
379                    }
380
381                    unset($user['access']);
382                }
383            }
384        }
385
386        $users = $this->enrichUsers($users);
387        $users = $this->enrichUsersWithLastSeen($users);
388
389        foreach ($users as &$user) {
390            unset($user['password']);
391        }
392
393        Common::sendHeader('X-Matomo-Total-Results: ' . $totalResults);
394        return $users;
395    }
396
397    /**
398     * Returns the list of all the users
399     *
400     * @param string $userLogins Comma separated list of users to select. If not specified, will return all users
401     * @return array the list of all the users
402     */
403    public function getUsers($userLogins = '')
404    {
405        Piwik::checkUserHasSomeAdminAccess();
406
407        if (!is_string($userLogins)) {
408            throw new \Exception('Parameter userLogins needs to be a string containing a comma separated list of users');
409        }
410
411        $logins = array();
412
413        if (!empty($userLogins)) {
414            $logins = explode(',', $userLogins);
415        }
416
417        $users = $this->model->getUsers($logins);
418        $users = $this->userFilter->filterUsers($users);
419        $users = $this->enrichUsers($users);
420
421        return $users;
422    }
423
424    /**
425     * Returns the list of all the users login
426     *
427     * @return array the list of all the users login
428     */
429    public function getUsersLogin()
430    {
431        Piwik::checkUserHasSomeAdminAccess();
432
433        $logins = $this->model->getUsersLogin();
434        $logins = $this->userFilter->filterLogins($logins);
435
436        return $logins;
437    }
438
439    /**
440     * For each user, returns the list of website IDs where the user has the supplied $access level.
441     * If a user doesn't have the given $access to any website IDs,
442     * the user will not be in the returned array.
443     *
444     * @param string Access can have the following values : 'view' or 'admin'
445     *
446     * @return array    The returned array has the format
447     *                    array(
448     *                        login1 => array ( idsite1,idsite2),
449     *                        login2 => array(idsite2),
450     *                        ...
451     *                    )
452     */
453    public function getUsersSitesFromAccess($access)
454    {
455        Piwik::checkUserHasSuperUserAccess();
456
457        $this->checkAccessType($access);
458
459        $userSites = $this->model->getUsersSitesFromAccess($access);
460        $userSites = $this->userFilter->filterLoginIndexedArray($userSites);
461
462        return $userSites;
463    }
464
465    /**
466     * Throws an exception if one of the given access types does not exists.
467     *
468     * @param string|array $access
469     * @throws Exception
470     */
471    private function checkAccessType($access)
472    {
473        $access = (array) $access;
474
475        foreach ($access as $entry) {
476            if (!$this->isValidAccessType($entry)) {
477                throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", [implode(", ", $this->getAllRolesAndCapabilities()), $entry]));
478            }
479        }
480    }
481
482    /**
483     * returns if the given access type exists
484     *
485     * @param string $access
486     * @return bool
487     */
488    private function isValidAccessType($access)
489    {
490        return in_array($access, $this->getAllRolesAndCapabilities(), true);
491    }
492
493    private function getAllRolesAndCapabilities()
494    {
495        $roles = $this->roleProvider->getAllRoleIds();
496        $capabilities = $this->capabilityProvider->getAllCapabilityIds();
497        return array_merge($roles, $capabilities);
498    }
499
500    /**
501     * For each user, returns their access level for the given $idSite.
502     * If a user doesn't have any access to the $idSite ('noaccess'),
503     * the user will not be in the returned array.
504     *
505     * @param int $idSite website ID
506     *
507     * @return array    The returned array has the format
508     *                    array(
509     *                        login1 => 'view',
510     *                        login2 => 'admin',
511     *                        login3 => 'view',
512     *                        ...
513     *                    )
514     */
515    public function getUsersAccessFromSite($idSite)
516    {
517        Piwik::checkUserHasAdminAccess($idSite);
518
519        $usersAccess = $this->model->getUsersAccessFromSite($idSite);
520        $usersAccess = $this->userFilter->filterLoginIndexedArray($usersAccess);
521
522        return $usersAccess;
523    }
524
525    public function getUsersWithSiteAccess($idSite, $access)
526    {
527        Piwik::checkUserHasAdminAccess($idSite);
528        $this->checkAccessType($access);
529
530        $logins = $this->model->getUsersLoginWithSiteAccess($idSite, $access);
531
532        if (empty($logins)) {
533            return array();
534        }
535
536        $logins = $this->userFilter->filterLogins($logins);
537        $logins = implode(',', $logins);
538
539        return $this->getUsers($logins);
540    }
541
542    /**
543     * For each website ID, returns the access level of the given $userLogin.
544     * If the user doesn't have any access to a website ('noaccess'),
545     * this website will not be in the returned array.
546     * If the user doesn't have any access, the returned array will be an empty array.
547     *
548     * @param string $userLogin User that has to be valid
549     *
550     * @return array    The returned array has the format
551     *                    array(
552     *                        idsite1 => 'view',
553     *                        idsite2 => 'admin',
554     *                        idsite3 => 'view',
555     *                        ...
556     *                    )
557     */
558    public function getSitesAccessFromUser($userLogin)
559    {
560        Piwik::checkUserHasSuperUserAccess();
561        $this->checkUserExists($userLogin);
562        // Super users have 'admin' access for every site
563        if (Piwik::hasTheUserSuperUserAccess($userLogin)) {
564            $return = array();
565            $siteManagerModel = new \Piwik\Plugins\SitesManager\Model();
566            $sites = $siteManagerModel->getAllSites();
567            foreach ($sites as $site) {
568                $return[] = array(
569                    'site' => $site['idsite'],
570                    'access' => 'admin'
571                );
572            }
573            return $return;
574        }
575        return $this->model->getSitesAccessFromUser($userLogin);
576    }
577
578    /**
579     * For each website ID, returns the access level of the given $userLogin (if the user is not a superuser).
580     * If the user doesn't have any access to a website ('noaccess'),
581     * this website will not be in the returned array.
582     * If the user doesn't have any access, the returned array will be an empty array.
583     *
584     * @param string $userLogin User that has to be valid
585     *
586     * @param int|null $limit
587     * @param int|null $offset
588     * @param string|null $filter_search text to search for in site name, URLs, or group.
589     * @param string|null $filter_access access level to select for, can be 'some', 'view' or 'admin' (by default 'some')
590     * @return array    The returned array has the format
591     *                    array(
592     *                        ['idsite' => 1, 'site_name' => 'the site', 'access' => 'admin'],
593     *                        ['idsite' => 2, 'site_name' => 'the other site', 'access' => 'view'],
594     *                        ...
595     *                    )
596     * @throws Exception
597     */
598    public function getSitesAccessForUser($userLogin, $limit = null, $offset = 0, $filter_search = null, $filter_access = null)
599    {
600        Piwik::checkUserHasSomeAdminAccess();
601        $this->checkUserExists($userLogin);
602
603        if (Piwik::hasTheUserSuperUserAccess($userLogin)) {
604            throw new \Exception("This method should not be used with superusers.");
605        }
606
607        $idSites = null;
608        if (!Piwik::hasUserSuperUserAccess()) {
609            $idSites = $this->access->getSitesIdWithAdminAccess();
610            if (empty($idSites)) { // sanity check
611                throw new \Exception("The current admin user does not have access to any sites.");
612            }
613        }
614
615        [$sites, $totalResults] = $this->model->getSitesAccessFromUserWithFilters($userLogin, $limit, $offset, $filter_search, $filter_access, $idSites);
616        foreach ($sites as &$siteAccess) {
617            [$siteAccess['role'], $siteAccess['capabilities']] = $this->getRoleAndCapabilitiesFromAccess($siteAccess['access']);
618            $siteAccess['role'] = empty($siteAccess['role']) ? 'noaccess' : reset($siteAccess['role']);
619            unset($siteAccess['access']);
620        }
621
622        $hasAccessToAny = $this->model->getSiteAccessCount($userLogin) > 0;
623
624        Common::sendHeader('X-Matomo-Total-Results: ' . $totalResults);
625        if ($hasAccessToAny) {
626            Common::sendHeader('X-Matomo-Has-Some: 1');
627        }
628        return $sites;
629    }
630
631    /**
632     * Returns the user information (login, password hash, email, date_registered, etc.)
633     *
634     * @param string $userLogin the user login
635     *
636     * @return array the user information
637     */
638    public function getUser($userLogin)
639    {
640        Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
641        $this->checkUserExists($userLogin);
642
643        $user = $this->model->getUser($userLogin);
644
645        $user = $this->userFilter->filterUser($user);
646        $user = $this->enrichUser($user);
647
648        return $user;
649    }
650
651    /**
652     * Returns the user information (login, password hash, email, date_registered, etc.)
653     *
654     * @param string $userEmail the user email
655     *
656     * @return array the user information
657     */
658    public function getUserByEmail($userEmail)
659    {
660        Piwik::checkUserHasSuperUserAccess();
661        $this->checkUserEmailExists($userEmail);
662
663        $user = $this->model->getUserByEmail($userEmail);
664
665        $user = $this->userFilter->filterUser($user);
666        $user = $this->enrichUser($user);
667
668        return $user;
669    }
670
671    private function checkLogin($userLogin)
672    {
673        if ($this->userExists($userLogin)) {
674            throw new Exception(Piwik::translate('UsersManager_ExceptionLoginExists', $userLogin));
675        }
676
677        if ($this->userEmailExists($userLogin)) {
678            throw new Exception(Piwik::translate('UsersManager_ExceptionLoginExistsAsEmail', $userLogin));
679        }
680
681        Piwik::checkValidLoginString($userLogin);
682    }
683
684    private function checkEmail($email, $userLogin = null)
685    {
686        if ($this->userEmailExists($email)) {
687            throw new Exception(Piwik::translate('UsersManager_ExceptionEmailExists', $email));
688        }
689
690        if ($userLogin && mb_strtolower($userLogin) !== mb_strtolower($email) && $this->userExists($email)) {
691            throw new Exception(Piwik::translate('UsersManager_ExceptionEmailExistsAsLogin', $email));
692        }
693
694        if (!$userLogin && $this->userExists($email)) {
695            throw new Exception(Piwik::translate('UsersManager_ExceptionEmailExistsAsLogin', $email));
696        }
697
698        if (!Piwik::isValidEmailString($email)) {
699            throw new Exception(Piwik::translate('UsersManager_ExceptionInvalidEmail'));
700        }
701    }
702
703    /**
704     * Add a user in the database.
705     * A user is defined by
706     * - a login that has to be unique and valid
707     * - a password that has to be valid
708     * - an email that has to be in a correct format
709     *
710     * @see userExists()
711     * @see isValidLoginString()
712     * @see isValidPasswordString()
713     * @see isValidEmailString()
714     *
715     * @exception in case of an invalid parameter
716     */
717    public function addUser($userLogin, $password, $email, $_isPasswordHashed = false, $initialIdSite = null)
718    {
719        Piwik::checkUserHasSomeAdminAccess();
720        UsersManager::dieIfUsersAdminIsDisabled();
721
722        if (!Piwik::hasUserSuperUserAccess()) {
723            if (empty($initialIdSite)) {
724                throw new \Exception(Piwik::translate("UsersManager_AddUserNoInitialAccessError"));
725            }
726
727            Piwik::checkUserHasAdminAccess($initialIdSite);
728        }
729
730        $this->checkLogin($userLogin);
731        $this->checkEmail($email);
732
733        $password = Common::unsanitizeInputValue($password);
734
735        if (!$_isPasswordHashed) {
736            UsersManager::checkPassword($password);
737
738            $passwordTransformed = UsersManager::getPasswordHash($password);
739        } else {
740            $passwordTransformed = $password;
741        }
742
743        $passwordTransformed = $this->password->hash($passwordTransformed);
744
745        $this->model->addUser($userLogin, $passwordTransformed, $email, Date::now()->getDatetime());
746
747        $container = StaticContainer::getContainer();
748        $mail = $container->make(UserCreatedEmail::class, array(
749            'login' => Piwik::getCurrentUserLogin(),
750            'emailAddress' => Piwik::getCurrentUserEmail(),
751            'userLogin' => $userLogin
752        ));
753        $mail->safeSend();
754
755        // we reload the access list which doesn't yet take in consideration this new user
756        Access::getInstance()->reloadAccess();
757        Cache::deleteTrackerCache();
758
759        /**
760         * Triggered after a new user is created.
761         *
762         * @param string $userLogin The new user's login handle.
763         */
764        Piwik::postEvent('UsersManager.addUser.end', array($userLogin, $email, $password));
765
766        if ($initialIdSite) {
767            $this->setUserAccess($userLogin, 'view', $initialIdSite);
768        }
769    }
770
771    /**
772     * Enable or disable Super user access to the given user login. Note: When granting Super User access all previous
773     * permissions of the user will be removed as the user gains access to everything.
774     *
775     * @param string   $userLogin          the user login.
776     * @param bool|int $hasSuperUserAccess true or '1' to grant Super User access, false or '0' to remove Super User
777     *                                     access.
778     * @param string $passwordConfirmation the current user's password. For security purposes, this value should be
779     *                                     sent as a POST parameter.
780     * @throws \Exception
781     */
782    public function setSuperUserAccess($userLogin, $hasSuperUserAccess, $passwordConfirmation = null)
783    {
784        Piwik::checkUserHasSuperUserAccess();
785        $this->checkUserIsNotAnonymous($userLogin);
786        UsersManager::dieIfUsersAdminIsDisabled();
787
788        $requirePasswordConfirmation = self::$SET_SUPERUSER_ACCESS_REQUIRE_PASSWORD_CONFIRMATION;
789        self::$SET_SUPERUSER_ACCESS_REQUIRE_PASSWORD_CONFIRMATION = true;
790
791        $isCliMode = Common::isPhpCliMode() && !(defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE);
792        if (!$isCliMode
793            && $requirePasswordConfirmation
794        ) {
795            $this->confirmCurrentUserPassword($passwordConfirmation);
796        }
797        $this->checkUserExists($userLogin);
798
799        if (!$hasSuperUserAccess && $this->isUserTheOnlyUserHavingSuperUserAccess($userLogin)) {
800            $message = Piwik::translate("UsersManager_ExceptionRemoveSuperUserAccessOnlySuperUser", $userLogin)
801                        . " "
802                        . Piwik::translate("UsersManager_ExceptionYouMustGrantSuperUserAccessFirst");
803            throw new Exception($message);
804        }
805
806        $this->model->deleteUserAccess($userLogin);
807        $this->model->setSuperUserAccess($userLogin, $hasSuperUserAccess);
808
809        Cache::deleteTrackerCache();
810    }
811
812    /**
813     * Detect whether the current user has super user access or not.
814     *
815     * @return bool
816     */
817    public function hasSuperUserAccess()
818    {
819        return Piwik::hasUserSuperUserAccess();
820    }
821
822    /**
823     * Returns a list of all Super Users containing there userLogin and email address.
824     *
825     * @return array
826     */
827    public function getUsersHavingSuperUserAccess()
828    {
829        Piwik::checkUserIsNotAnonymous();
830
831        $users = $this->model->getUsersHavingSuperUserAccess();
832        $users = $this->enrichUsers($users);
833
834        // we do not filter these users by access and return them all since we need to print this information in the
835        // UI and they are allowed to see this.
836
837        return $users;
838    }
839
840    private function enrichUsersWithLastSeen($users)
841    {
842        $formatter = new Formatter();
843
844        $lastSeenTimes = LastSeenTimeLogger::getLastSeenTimesForAllUsers();
845        foreach ($users as &$user) {
846            $login = $user['login'];
847            if (isset($lastSeenTimes[$login])) {
848                $user['last_seen'] = $formatter->getPrettyTimeFromSeconds(time() - $lastSeenTimes[$login]);
849            }
850        }
851        return $users;
852    }
853
854    private function enrichUsers($users)
855    {
856        if (!empty($users)) {
857            foreach ($users as $index => $user) {
858                $users[$index] = $this->enrichUser($user);
859            }
860        }
861        return $users;
862    }
863
864    private function isTwoFactorAuthPluginEnabled()
865    {
866        if (!isset($this->twoFaPluginActivated)) {
867            $this->twoFaPluginActivated = Plugin\Manager::getInstance()->isPluginActivated('TwoFactorAuth');
868        }
869        return $this->twoFaPluginActivated;
870    }
871
872    private function enrichUser($user)
873    {
874        if (empty($user)) {
875            return $user;
876        }
877
878        unset($user['token_auth']);
879        unset($user['password']);
880        unset($user['ts_password_modified']);
881
882        if ($lastSeen = LastSeenTimeLogger::getLastSeenTimeForUser($user['login'])) {
883            $user['last_seen'] = Date::getDatetimeFromTimestamp($lastSeen);
884        }
885
886        if (Piwik::hasUserSuperUserAccess()) {
887            $user['uses_2fa'] = !empty($user['twofactor_secret']) && $this->isTwoFactorAuthPluginEnabled();
888            unset($user['twofactor_secret']);
889            return $user;
890        }
891
892        $newUser = array('login' => $user['login']);
893
894        if ($user['login'] === Piwik::getCurrentUserLogin() || !empty($user['superuser_access'])) {
895            $newUser['email'] = $user['email'];
896        }
897
898        if (isset($user['role'])) {
899            $newUser['role'] = $user['role'] == 'superuser' ? 'admin' : $user['role'];
900        }
901        if (isset($user['capabilities'])) {
902            $newUser['capabilities'] = $user['capabilities'];
903        }
904
905        if (isset($user['superuser_access'])) {
906            $newUser['superuser_access'] = $user['superuser_access'];
907        }
908
909        if (isset($user['last_seen'])) {
910            $newUser['last_seen'] = $user['last_seen'];
911        }
912
913        return $newUser;
914    }
915
916    /**
917     * Updates a user in the database.
918     * Only login and password are required (case when we update the password).
919     *
920     * If password or email changes, it is required to also specify the password of the current user needs to be specified
921     * to confirm this change.
922     *
923     * @see addUser() for all the parameters
924     */
925    public function updateUser($userLogin, $password = false, $email = false,
926                               $_isPasswordHashed = false, $passwordConfirmation = false)
927    {
928        $requirePasswordConfirmation = self::$UPDATE_USER_REQUIRE_PASSWORD_CONFIRMATION;
929        self::$UPDATE_USER_REQUIRE_PASSWORD_CONFIRMATION = true;
930
931        $isEmailNotificationOnInConfig = Config::getInstance()->General['enable_update_users_email'];
932
933        Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
934        UsersManager::dieIfUsersAdminIsDisabled();
935        $this->checkUserIsNotAnonymous($userLogin);
936        $this->checkUserExists($userLogin);
937
938        $userInfo   = $this->model->getUser($userLogin);
939        $changeShouldRequirePasswordConfirmation = false;
940
941        $passwordHasBeenUpdated = false;
942
943        if (empty($password)) {
944            $password = false;
945        } else {
946            $changeShouldRequirePasswordConfirmation = true;
947            $password = Common::unsanitizeInputValue($password);
948
949            if (!$_isPasswordHashed) {
950                UsersManager::checkPassword($password);
951                $password = UsersManager::getPasswordHash($password);
952            }
953
954            $passwordInfo = $this->password->info($password);
955
956            if (!isset($passwordInfo['algo']) || 0 >= $passwordInfo['algo']) {
957                // password may have already been fully hashed
958                $password = $this->password->hash($password);
959            }
960
961            $passwordHasBeenUpdated = true;
962        }
963
964        if (empty($email)) {
965            $email = $userInfo['email'];
966        }
967
968        $hasEmailChanged = mb_strtolower($email) !== mb_strtolower($userInfo['email']);
969
970        if ($hasEmailChanged) {
971            $this->checkEmail($email, $userLogin);
972            $changeShouldRequirePasswordConfirmation = true;
973        }
974
975        if ($changeShouldRequirePasswordConfirmation && $requirePasswordConfirmation) {
976            $this->confirmCurrentUserPassword($passwordConfirmation);
977        }
978
979        $this->model->updateUser($userLogin, $password, $email);
980
981        Cache::deleteTrackerCache();
982
983        if ($hasEmailChanged && $isEmailNotificationOnInConfig) {
984            $this->sendEmailChangedEmail($userInfo, $email);
985        }
986
987        if ($passwordHasBeenUpdated && $requirePasswordConfirmation && $isEmailNotificationOnInConfig) {
988            $this->sendPasswordChangedEmail($userInfo);
989        }
990
991        /**
992         * Triggered after an existing user has been updated.
993         * Event notify about password change.
994         *
995         * @param string $userLogin The user's login handle.
996         * @param boolean $passwordHasBeenUpdated Flag containing information about password change.
997         */
998        Piwik::postEvent('UsersManager.updateUser.end', array($userLogin, $passwordHasBeenUpdated, $email, $password));
999    }
1000
1001    /**
1002     * Delete one or more users and all its access, given its login.
1003     *
1004     * @param string $userLogin the user login(s).
1005     *
1006     * @throws Exception if the user doesn't exist or if deleting the users would leave no superusers.
1007     *
1008     * @return bool true on success
1009     */
1010    public function deleteUser($userLogin)
1011    {
1012        Piwik::checkUserHasSuperUserAccess();
1013        UsersManager::dieIfUsersAdminIsDisabled();
1014        $this->checkUserIsNotAnonymous($userLogin);
1015
1016        $this->checkUserExist($userLogin);
1017
1018        if ($this->isUserTheOnlyUserHavingSuperUserAccess($userLogin)) {
1019            $message = Piwik::translate("UsersManager_ExceptionDeleteOnlyUserWithSuperUserAccess", $userLogin)
1020                        . " "
1021                        . Piwik::translate("UsersManager_ExceptionYouMustGrantSuperUserAccessFirst");
1022            throw new Exception($message);
1023        }
1024
1025        $this->model->deleteUserOnly($userLogin);
1026        $this->model->deleteUserOptions($userLogin);
1027        $this->model->deleteUserAccess($userLogin);
1028
1029        $container = StaticContainer::getContainer();
1030        $email = $container->make(UserDeletedEmail::class, array(
1031            'login' => Piwik::getCurrentUserLogin(),
1032            'emailAddress' => Piwik::getCurrentUserEmail(),
1033            'userLogin' => $userLogin
1034        ));
1035        $email->safeSend();
1036
1037        Cache::deleteTrackerCache();
1038    }
1039
1040    /**
1041     * Returns true if the given userLogin is known in the database
1042     *
1043     * @param string $userLogin
1044     * @return bool true if the user is known
1045     */
1046    public function userExists($userLogin)
1047    {
1048        if ($userLogin == 'anonymous') {
1049            return true;
1050        }
1051
1052        Piwik::checkUserIsNotAnonymous();
1053        Piwik::checkUserHasSomeViewAccess();
1054
1055        if ($userLogin == Piwik::getCurrentUserLogin()) {
1056            return true;
1057        }
1058
1059        return $this->model->userExists($userLogin);
1060    }
1061
1062    /**
1063     * Returns true if user with given email (userEmail) is known in the database, or the Super User
1064     *
1065     * @param string $userEmail
1066     * @return bool true if the user is known
1067     */
1068    public function userEmailExists($userEmail)
1069    {
1070        Piwik::checkUserIsNotAnonymous();
1071        Piwik::checkUserHasSomeViewAccess();
1072
1073        return $this->model->userEmailExists($userEmail);
1074    }
1075
1076    /**
1077     * Returns the first login name of an existing user that has the given email address. If no user can be found for
1078     * this user an error will be returned.
1079     *
1080     * @param string $userEmail
1081     * @return bool true if the user is known
1082     */
1083    public function getUserLoginFromUserEmail($userEmail)
1084    {
1085        Piwik::checkUserIsNotAnonymous();
1086        Piwik::checkUserHasSomeAdminAccess();
1087
1088        $this->checkUserEmailExists($userEmail);
1089
1090        $user = $this->model->getUserByEmail($userEmail);
1091
1092        // any user with some admin access is allowed to find any user by email, no need to filter by access here
1093
1094        return $user['login'];
1095    }
1096
1097    /**
1098     * Set an access level to a given user for a list of websites ID.
1099     *
1100     * If access = 'noaccess' the current access (if any) will be deleted.
1101     * If access = 'view' or 'admin' the current access level is deleted and updated with the new value.
1102     *
1103     * @param string $userLogin The user login
1104     * @param string|array $access Access to grant. Must have one of the following value : noaccess, view, write, admin.
1105     *                              May also be an array to sent additional capabilities
1106     * @param int|array $idSites The array of idSites on which to apply the access level for the user.
1107     *       If the value is "all" then we apply the access level to all the websites ID for which the current authentificated user has an 'admin' access.
1108     * @throws Exception if the user doesn't exist
1109     * @throws Exception if the access parameter doesn't have a correct value
1110     * @throws Exception if any of the given website ID doesn't exist
1111     */
1112    public function setUserAccess($userLogin, $access, $idSites)
1113    {
1114        UsersManager::dieIfUsersAdminIsDisabled();
1115
1116        if ($access != 'noaccess') {
1117            $this->checkAccessType($access);
1118        }
1119
1120        $idSites = $this->getIdSitesCheckAdminAccess($idSites);
1121
1122        if ($userLogin === 'anonymous' &&
1123            (is_array($access) || !in_array($access, array('view', 'noaccess'), true))
1124        ) {
1125            throw new Exception(Piwik::translate("UsersManager_ExceptionAnonymousAccessNotPossible", array('noaccess', 'view')));
1126        }
1127
1128        $roles = array();
1129        $capabilities = array();
1130
1131        if (is_array($access)) {
1132            // we require one role, and optionally multiple capabilities
1133            [$roles, $capabilities] = $this->getRoleAndCapabilitiesFromAccess($access);
1134
1135            if (count($roles) < 1) {
1136                $ids = implode(', ', $this->roleProvider->getAllRoleIds());
1137                throw new Exception(Piwik::translate('UsersManager_ExceptionNoRoleSet', $ids));
1138            }
1139
1140            if (count($roles) > 1) {
1141                $ids = implode(', ', $this->roleProvider->getAllRoleIds());
1142                throw new Exception(Piwik::translate('UsersManager_ExceptionMultipleRoleSet', $ids));
1143            }
1144
1145        } else {
1146            // as only one access is set, we require it to be a role or "noaccess"...
1147            if ($access !== 'noaccess') {
1148                $this->roleProvider->checkValidRole($access);
1149                $roles[] = $access;
1150            }
1151        }
1152
1153        $this->checkUserExist($userLogin);
1154        $this->checkUsersHasNotSuperUserAccess($userLogin);
1155
1156        $this->model->deleteUserAccess($userLogin, $idSites);
1157
1158        if ($access === 'noaccess') {
1159            // if the access is noaccess then we don't save it as this is the default value
1160            // when no access are specified
1161            Piwik::postEvent('UsersManager.removeSiteAccess', array($userLogin, $idSites));
1162        } else {
1163            $role = array_shift($roles);
1164            $this->model->addUserAccess($userLogin, $role, $idSites);
1165        }
1166
1167        if (!empty($capabilities)) {
1168            $this->addCapabilities($userLogin, $capabilities, $idSites);
1169        }
1170
1171        // we reload the access list which doesn't yet take in consideration this new user access
1172        $this->reloadPermissions();
1173    }
1174
1175    /**
1176     * Adds the given capabilities to the given user for the given sites.
1177     * The capability will be added only when the user also has access to a site, for example View, Write, or Admin.
1178     * Note: You can neither add any capability to a super user, nor to the anonymous user.
1179     * Note: If the user has assigned a role which already grants the given capability, the capability will not be added in
1180     * the backend.
1181     *
1182     * @param string $userLogin The user login
1183     * @param string|string[] $capabilities  To fetch a list of available capabilities call "UsersManager.getAvailableCapabilities".
1184     * @param int|int[] $idSites
1185     * @throws Exception
1186     */
1187    public function addCapabilities($userLogin, $capabilities, $idSites)
1188    {
1189        $idSites = $this->getIdSitesCheckAdminAccess($idSites);
1190
1191        if ($userLogin == 'anonymous') {
1192            throw new Exception(Piwik::translate("UsersManager_ExceptionAnonymousNoCapabilities"));
1193        }
1194
1195        $this->checkUserExists($userLogin);
1196        $this->checkUsersHasNotSuperUserAccess([$userLogin]);
1197
1198        if (!is_array($capabilities)){
1199            $capabilities = array($capabilities);
1200        }
1201
1202        foreach ($capabilities as $entry) {
1203            $this->capabilityProvider->checkValidCapability($entry);
1204        }
1205
1206        [$sitesIdWithRole, $sitesIdWithCapability] = $this->getRolesAndCapabilitiesForLogin($userLogin);
1207
1208        foreach ($capabilities as $entry) {
1209            $cap = $this->capabilityProvider->getCapability($entry);
1210
1211            foreach ($idSites as $idSite) {
1212                $hasRole = array_key_exists($idSite, $sitesIdWithRole);
1213                $hasCapabilityAlready = array_key_exists($idSite, $sitesIdWithCapability) && in_array($entry, $sitesIdWithCapability[$idSite], true);
1214
1215                // so far we are adding the capability only to people that also have a role...
1216                // to be defined how to handle this... eg we are not throwing an exception currently
1217                // as it might be used as part of bulk action etc.
1218                if ($hasRole && !$hasCapabilityAlready) {
1219                    $theRole = $sitesIdWithRole[$idSite];
1220                    if ($cap->hasRoleCapability($theRole)) {
1221                        // todo this behaviour needs to be defined...
1222                        // when the role already supports this capability we do not add it again
1223                        continue;
1224                    }
1225
1226                    $this->model->addUserAccess($userLogin, $entry, array($idSite));
1227                }
1228            }
1229
1230        }
1231
1232        // we reload the access list which doesn't yet take in consideration this new user access
1233        $this->reloadPermissions();
1234    }
1235
1236    private function getRolesAndCapabilitiesForLogin($userLogin)
1237    {
1238        $sites = $this->model->getSitesAccessFromUser($userLogin);
1239        $roleIds = $this->roleProvider->getAllRoleIds();
1240
1241        $sitesIdWithRole = array();
1242        $sitesIdWithCapability = array();
1243        foreach ($sites as $site) {
1244            if (in_array($site['access'], $roleIds, true)) {
1245                $sitesIdWithRole[(int) $site['site']] = $site['access'];
1246            } else {
1247                if (!isset($sitesIdWithCapability[(int) $site['site']])) {
1248                    $sitesIdWithCapability[(int) $site['site']] = array();
1249                }
1250                $sitesIdWithCapability[(int) $site['site']][] = $site['access'];
1251            }
1252        }
1253        return [$sitesIdWithRole, $sitesIdWithCapability];
1254    }
1255
1256    /**
1257     * Removes the given capabilities from the given user for the given sites.
1258     * The capability will be only removed if it is actually granted as a separate capability. If the user has a role
1259     * that includes a specific capability, for example "Admin", then the capability will not be removed because the
1260     * assigned role will always include this capability.
1261     *
1262     * @param string $userLogin The user login
1263     * @param string|string[] $capabilities  To fetch a list of available capabilities call "UsersManager.getAvailableCapabilities".
1264     * @param int|int[] $idSites
1265     * @throws Exception
1266     */
1267    public function removeCapabilities($userLogin, $capabilities, $idSites)
1268    {
1269        $idSites = $this->getIdSitesCheckAdminAccess($idSites);
1270
1271        $this->checkUserExists($userLogin);
1272
1273        if (!is_array($capabilities)){
1274            $capabilities = array($capabilities);
1275        }
1276
1277        foreach ($capabilities as $capability) {
1278            $this->capabilityProvider->checkValidCapability($capability);
1279        }
1280
1281        foreach ($capabilities as $capability) {
1282            $this->model->removeUserAccess($userLogin, $capability, $idSites);
1283        }
1284
1285        // we reload the access list which doesn't yet take in consideration this removed capability
1286        $this->reloadPermissions();
1287    }
1288
1289    private function reloadPermissions()
1290    {
1291        Access::getInstance()->reloadAccess();
1292        Cache::deleteTrackerCache();
1293    }
1294
1295    private function getIdSitesCheckAdminAccess($idSites)
1296    {
1297        // in case idSites is all we grant access to all the websites on which the current connected user has an 'admin' access
1298        if ($idSites === 'all') {
1299            $idSites = \Piwik\Plugins\SitesManager\API::getInstance()->getSitesIdWithAdminAccess();
1300        } // in case the idSites is an integer we build an array
1301        else {
1302            $idSites = Site::getIdSitesFromIdSitesString($idSites);
1303        }
1304
1305        if (empty($idSites)) {
1306            throw new Exception('Specify at least one website ID in &idSites=');
1307        }
1308
1309        // it is possible to set user access on websites only for the websites admin
1310        // basically an admin can give the view or the admin access to any user for the websites they manage
1311        Piwik::checkUserHasAdminAccess($idSites);
1312
1313        if (!is_array($idSites)) {
1314            $idSites = array($idSites);
1315        }
1316
1317        return $idSites;
1318    }
1319
1320    /**
1321     * Throws an exception is the user login doesn't exist
1322     *
1323     * @param string $userLogin user login
1324     * @throws Exception if the user doesn't exist
1325     */
1326    private function checkUserExists($userLogin)
1327    {
1328        if (!$this->userExists($userLogin)) {
1329            throw new Exception(Piwik::translate("UsersManager_ExceptionUserDoesNotExist", $userLogin));
1330        }
1331    }
1332
1333    /**
1334     * Throws an exception is the user email cannot be found
1335     *
1336     * @param string $userEmail user email
1337     * @throws Exception if the user doesn't exist
1338     */
1339    private function checkUserEmailExists($userEmail)
1340    {
1341        if (!$this->userEmailExists($userEmail)) {
1342            throw new Exception(Piwik::translate("UsersManager_ExceptionUserDoesNotExist", $userEmail));
1343        }
1344    }
1345
1346    private function checkUserIsNotAnonymous($userLogin)
1347    {
1348        if ($userLogin == 'anonymous') {
1349            throw new Exception(Piwik::translate("UsersManager_ExceptionEditAnonymous"));
1350        }
1351    }
1352
1353    private function checkUsersHasNotSuperUserAccess($userLogins)
1354    {
1355        $userLogins = (array) $userLogins;
1356        $superusers = $this->getUsersHavingSuperUserAccess();
1357        $superusers = array_column($superusers, null, 'login');
1358
1359        foreach ($userLogins as $userLogin) {
1360            if (isset($superusers[$userLogin])) {
1361                throw new Exception(Piwik::translate("UsersManager_ExceptionUserHasSuperUserAccess", $userLogin));
1362            }
1363        }
1364    }
1365
1366    /**
1367     * @param string|string[] $userLogin
1368     * @return bool
1369     */
1370    private function isUserTheOnlyUserHavingSuperUserAccess($userLogin)
1371    {
1372        if (!is_array($userLogin)) {
1373            $userLogin = [$userLogin];
1374        }
1375
1376        $superusers = $this->getUsersHavingSuperUserAccess();
1377        $superusersByLogin = array_column($superusers, null, 'login');
1378
1379        foreach ($userLogin as $login) {
1380            unset($superusersByLogin[$login]);
1381        }
1382
1383        return empty($superusersByLogin);
1384    }
1385
1386    /**
1387     * Generates an app specific API token every time you call this method. You should ideally store this token securely
1388     * in your app and not generate a new token every time.
1389     *
1390     * If the username/password combination is incorrect an invalid token will be returned.
1391     *
1392     * @param string $userLogin Login or Email address
1393     * @param string $passwordConfirmation the current user's password. For security purposes, this value should be
1394     *                                     sent as a POST parameter.
1395     * @param string $description The description for this app specific password, for example your app name. Max 100 characters are allowed
1396     * @param string $expireDate Optionally a date when the token should expire
1397     * @param string $expireHours Optionally number of hours for how long the token should be valid before it expires.
1398     *                            If expireDate is set and expireHours, then expireDate will be used.
1399     *                            If expireDate is set and expireHours, then expireDate will be used.
1400     * @return string
1401     */
1402    public function createAppSpecificTokenAuth($userLogin, $passwordConfirmation, $description, $expireDate = null, $expireHours = 0)
1403    {
1404        $user = $this->model->getUser($userLogin);
1405        if (empty($user) && Piwik::isValidEmailString($userLogin)) {
1406            $user = $this->model->getUserByEmail($userLogin);
1407            if (!empty($user['login'])) {
1408                $userLogin = $user['login'];
1409            }
1410        }
1411
1412        if (empty($user) || !$this->passwordVerifier->isPasswordCorrect($userLogin, $passwordConfirmation)) {
1413            if (empty($user)) {
1414                /**
1415                 * @ignore
1416                 * @internal
1417                 */
1418                Piwik::postEvent('Login.authenticate.failed', array($userLogin));
1419            }
1420
1421            throw new \Exception(Piwik::translate('UsersManager_CurrentPasswordNotCorrect'));
1422        }
1423
1424        if (empty($expireDate) && !empty($expireHours) && is_numeric($expireHours)) {
1425            $expireDate = Date::now()->addHour($expireHours)->getDatetime();
1426        } elseif (!empty($expireDate)) {
1427            $expireDate = Date::factory($expireDate)->getDatetime();
1428        }
1429
1430        $generatedToken = $this->model->generateRandomTokenAuth();
1431        $this->model->addTokenAuth($userLogin, $generatedToken, $description, Date::now()->getDatetime(), $expireDate);
1432
1433        return $generatedToken;
1434    }
1435
1436    public function newsletterSignup()
1437    {
1438        Piwik::checkUserIsNotAnonymous();
1439
1440        $userLogin = Piwik::getCurrentUserLogin();
1441        $email = Piwik::getCurrentUserEmail();
1442
1443        $success = NewsletterSignup::signupForNewsletter($userLogin, $email, true);
1444        $result = $success ? array('success' => true) : array('error' => true);
1445        return $result;
1446    }
1447
1448    private function isUserHasAdminAccessTo($idSite)
1449    {
1450        try {
1451            Piwik::checkUserHasAdminAccess([$idSite]);
1452            return true;
1453        } catch (NoAccessException $ex) {
1454            return false;
1455        }
1456    }
1457
1458    private function checkUserExist($userLogin)
1459    {
1460        $userExists = $this->model->userExists($userLogin);
1461        if (!$userExists) {
1462            throw new Exception(Piwik::translate("UsersManager_ExceptionUserDoesNotExist", $userLogin));
1463        }
1464    }
1465
1466    private function getRoleAndCapabilitiesFromAccess($access)
1467    {
1468        $roles = [];
1469        $capabilities = [];
1470
1471        foreach ($access as $entry) {
1472            if (empty($entry)) {
1473                continue;
1474            }
1475
1476            if ($this->roleProvider->isValidRole($entry)) {
1477                $roles[] = $entry;
1478            } else {
1479                if ($this->isValidAccessType($entry)) {
1480                    $capabilities[] = $entry;
1481                }
1482            }
1483        }
1484        return [$roles, $capabilities];
1485    }
1486
1487    private function confirmCurrentUserPassword($passwordConfirmation)
1488    {
1489        if (empty($passwordConfirmation)) {
1490            throw new Exception(Piwik::translate('UsersManager_ConfirmWithPassword'));
1491        }
1492
1493        $passwordConfirmation = Common::unsanitizeInputValue($passwordConfirmation);
1494
1495        $loginCurrentUser = Piwik::getCurrentUserLogin();
1496        if (!$this->passwordVerifier->isPasswordCorrect($loginCurrentUser, $passwordConfirmation)) {
1497            throw new Exception(Piwik::translate('UsersManager_CurrentPasswordNotCorrect'));
1498        }
1499    }
1500
1501    private function sendEmailChangedEmail($user, $newEmail)
1502    {
1503        // send the mail to both the old email and the new email
1504        foreach ([$newEmail, $user['email']] as $emailTo) {
1505            $this->sendUserInfoChangedEmail('email', $user, $newEmail, $emailTo, 'UsersManager_EmailChangeNotificationSubject');
1506        }
1507    }
1508
1509    private function sendUserInfoChangedEmail($type, $user, $newValue, $emailTo, $subject)
1510    {
1511        $deviceDescription = $this->getDeviceDescription();
1512
1513        $view = new View('@UsersManager/_userInfoChangedEmail.twig');
1514        $view->type = $type;
1515        $view->accountName = Common::sanitizeInputValue($user['login']);
1516        $view->newEmail = Common::sanitizeInputValue($newValue);
1517        $view->ipAddress = IP::getIpFromHeader();
1518        $view->deviceDescription = $deviceDescription;
1519
1520        $mail = new Mail();
1521
1522        $mail->addTo($emailTo, $user['login']);
1523        $mail->setSubject(Piwik::translate($subject));
1524        $mail->setDefaultFromPiwik();
1525        $mail->setWrappedHtmlBody($view);
1526
1527        $replytoEmailName = Config::getInstance()->General['login_password_recovery_replyto_email_name'];
1528        $replytoEmailAddress = Config::getInstance()->General['login_password_recovery_replyto_email_address'];
1529        $mail->addReplyTo($replytoEmailAddress, $replytoEmailName);
1530
1531        $mail->send();
1532    }
1533
1534    private function sendPasswordChangedEmail($user)
1535    {
1536        $this->sendUserInfoChangedEmail('password', $user, null, $user['email'], 'UsersManager_PasswordChangeNotificationSubject');
1537    }
1538
1539    private function getDeviceDescription()
1540    {
1541        $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
1542
1543        $uaParser = new DeviceDetector($userAgent);
1544        $uaParser->parse();
1545
1546        $deviceName = ucfirst($uaParser->getDeviceName());
1547        if (!empty($deviceName)) {
1548            $description = $deviceName;
1549        } else {
1550            $description = Piwik::translate('General_Unknown');
1551        }
1552
1553        $deviceBrand = $uaParser->getBrandName();
1554        $deviceModel = $uaParser->getModel();
1555        if (!empty($deviceBrand)
1556            || !empty($deviceModel)
1557        ) {
1558            $parts = array_filter([$deviceBrand, $deviceModel]);
1559            $description .= ' (' . implode(' ', $parts) . ')';
1560        }
1561
1562        return $description;
1563    }
1564}
1565