Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiosmosis <diosmosis@users.noreply.github.com>2018-08-07 01:20:32 +0300
committerGitHub <noreply@github.com>2018-08-07 01:20:32 +0300
commit2e006803ee17a8d1a992085c6425eddaa84a25f5 (patch)
tree6798441780992ff6e1b3010aa98519c2c2af1f5b
parentbb1b1b4b068fc40da896e7eedbad3b2914dc2468 (diff)
Scalable UX for user management (#13158)
* Create empty components. * Mock up users list pagination. * Finish initial version of mockup. * Tweak to UI * More UI changes to new users manager screen. * More UI changes * Mock up user permission edits. * More tweaks to user permission editing (on both edit form & in users table). * add options * Another iteration on the UsersManager UI. * Update UsersManager UI again. * Implementing parts of the UI, fixing issue w/ overlapping material selects, creating dropdown directives for dropdown w/ submenu using materializecss, change bulk actions to be dropdown button. * Merge menu/submenu directives. * More superuser UI only functionality. * Fill out more logic of users manager UI + merging extra unneeded components/directives. * More users manager UI only changes. * Incomplete API method for new users list page. * Fill in server side pagination logic w/ tests & generally get to work in UI. * Make sure selects w/ placeholders can be unset. * Add loading state to users list + fix pagination issues + resize pagination in case the numbers are large. * Add last seen time to getUsersPlusAccessLevel() so it displays in UI. * Add permission edit pagination AJAX query + server side code. * Add "add access" button to user permission component. * Change permissions column to role + remove superuser checkbox & merge w/ Role column. * Delete user + bulk delete functionality. * Get delete users to work when entire search is selected. * Ask for confirmation before setting access in users list & implement access change logic. * Get bulk access functionality on users list to work (w/ tests). * Fix a bug in user table filtering + get permissions edit search to work. * Complete logic for permissions edit. * Change add user workflow so we do not have to save each permission edit in memory before saving whole user. * Add/edit user functionality. * Toggle superuser access functionality + some modal fixes. * in users list display ajax loading notification so counter is not changed visibly before rows are loaded. * initial review changes, disable functionality when viewing user is not superuser and some UI tweaks. * Redo top controls for user permission edit and add slide up toast notification for when a site is added. * Display warning in user permission edit if user has no access at all. * Do not reload users after going back from user edit form. * Force giving a new user access to a site when creating a user and make sure user list reloads if a user is modified, but does not realod if no user is modified. * Add form help to the non-straightforward fields. * Remove old usersmanager code & fix pagination bug. * Add help icon explaining roles to users list + permission edit. * Allow admin users to create other users + fix some regressions when making page-users-list not reload every time. * Apply self review changes. * Do not allow editing user details when an admin user edits a user. * Starting on UI tests. * Limit users displayed in page list to those that already have access to sites the current user is an admin of. * Refactor bulk/single AJAX calls & redraw component boundaries (users manager component owns user search state, paged users list owns table/control state). * Get add existing user modal to work. * write most UI tests + modify fixture * Fill out rest of UI test suite & get the rest to pass. * fix couple regressions * Get UI tests to pass and start on translation. * adding translations * try to fix some tests * Fixing API tests. * Fixing UsersManager tests. * Fix UI tests. * Add capabilities to new API output. * remove non-existant file references. * Add Write role to dropdowns. * Select from proper join. * tweak test * Updating UI tests. * Change styling of user permissions edit. * Update screenshots * Apply some PR feedback. * apply some review feedback * more review changes * update file headers * remove some TODOs * fix some tests * some more review fixes * update test files * Fix failing tests.
-rw-r--r--core/Access.php46
-rw-r--r--core/Access/CapabilitiesProvider.php15
-rw-r--r--plugins/CoreHome/CoreHome.php5
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.js3
-rw-r--r--plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.js56
-rw-r--r--plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less27
-rw-r--r--plugins/CoreHome/angularjs/notification/notification.directive.js7
-rw-r--r--plugins/CoreHome/angularjs/siteselector/siteselector.directive.html4
-rw-r--r--plugins/CoreHome/angularjs/siteselector/siteselector.directive.js3
-rw-r--r--plugins/CoreHome/angularjs/siteselector/siteselector.directive.less5
-rw-r--r--plugins/CoreHome/javascripts/notification.js92
-rw-r--r--plugins/CorePluginsAdmin/angularjs/field/field.directive.js2
-rw-r--r--plugins/CorePluginsAdmin/angularjs/form-field/field-select.html6
-rw-r--r--plugins/CorePluginsAdmin/angularjs/form-field/field-site.html7
-rw-r--r--plugins/CorePluginsAdmin/angularjs/form-field/form-field.directive.js38
-rw-r--r--plugins/Morpheus/stylesheets/general/_form.less8
-rw-r--r--plugins/SitesManager/Model.php16
-rw-r--r--plugins/UsersManager/API.php309
-rw-r--r--plugins/UsersManager/Controller.php112
-rw-r--r--plugins/UsersManager/LastSeenTimeLogger.php12
-rw-r--r--plugins/UsersManager/Model.php195
-rw-r--r--plugins/UsersManager/Sql/SiteAccessFilter.php91
-rw-r--r--plugins/UsersManager/Sql/UserTableFilter.php119
-rw-r--r--plugins/UsersManager/UsersManager.php69
-rw-r--r--plugins/UsersManager/angularjs/give-user-view-access/give-user-view-access.controller.js176
-rw-r--r--plugins/UsersManager/angularjs/manage-super-user/manage-super-user.controller.js76
-rw-r--r--plugins/UsersManager/angularjs/manage-user-access/manage-user-access.controller.js126
-rw-r--r--plugins/UsersManager/angularjs/manage-users/manage-users.controller.js217
-rw-r--r--plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.html201
-rw-r--r--plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.js213
-rw-r--r--plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.less203
-rw-r--r--plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.html145
-rw-r--r--plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.js140
-rw-r--r--plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.less43
-rw-r--r--plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.html152
-rw-r--r--plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.js262
-rw-r--r--plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.less213
-rw-r--r--plugins/UsersManager/angularjs/users-manager/users-manager.component.html80
-rw-r--r--plugins/UsersManager/angularjs/users-manager/users-manager.component.js211
-rw-r--r--plugins/UsersManager/angularjs/users-manager/users-manager.component.less28
-rw-r--r--plugins/UsersManager/lang/en.json53
-rw-r--r--plugins/UsersManager/templates/index.twig316
-rw-r--r--plugins/UsersManager/tests/Fixtures/ManyUsers.php95
-rw-r--r--plugins/UsersManager/tests/Integration/APITest.php318
-rw-r--r--plugins/UsersManager/tests/Integration/UsersManagerTest.php85
-rw-r--r--plugins/UsersManager/tests/System/ApiTest.php2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login1_when_superuseraccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_adminaccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_superuseraccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_superuseraccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_viewaccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login6_when_superuseraccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_adminaccess.xml1
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_superuseraccess.xml1
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_adminaccess.xml1
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_superuseraccess.xml2
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersSitesFromAccess_admin_when_superuseraccess.xml5
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersWithSiteAccess_3_admin_when_superuseraccess.xml4
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_adminaccess.xml4
-rw-r--r--plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_superuseraccess.xml28
-rw-r--r--plugins/UsersManager/tests/UI/UsersManager_spec.js504
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_add_new_user_form.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_email.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_login.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_not_exists.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_edit_permissions.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_existing_user_modal.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_load.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_edit_user_basic_info.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_confirmation.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_loaded.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_form_opened.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_no_user_entered.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_already_has_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_not_found.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_email.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_login.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search_deselected.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_selected.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_remove_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access_confirm.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_bulk_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_single.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_edit_user_form.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_filters.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_load.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_manage_users_back.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_next_click.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_rows_in_search.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_sites_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set_all.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_edit.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_filters.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_next.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_single.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_all.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_multiple.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_single_site_access.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_previous.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_role_for.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_rows_selected.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_confirm.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_set.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_tab.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_user_created.png3
-rw-r--r--tests/PHPUnit/Integration/AccessTest.php69
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_admin_manage_users.png3
-rw-r--r--tests/UI/specs/UIIntegration_spec.js16
113 files changed, 4123 insertions, 1276 deletions
diff --git a/core/Access.php b/core/Access.php
index 3f55fb4151..d7514af8d8 100644
--- a/core/Access.php
+++ b/core/Access.php
@@ -605,6 +605,52 @@ class Access
return $result;
}
+
+ /**
+ * Returns the level of access the current user has to the given site.
+ *
+ * @param int $idSite The site to check.
+ * @return string The access level, eg, 'view', 'admin', 'noaccess'.
+ */
+ public function getRoleForSite($idSite)
+ {
+ if ($this->hasSuperUserAccess
+ || in_array($idSite, $this->getSitesIdWithAdminAccess())
+ ) {
+ return 'admin';
+ }
+
+ if (in_array($idSite, $this->getSitesIdWithWriteAccess())) {
+ return 'write';
+ }
+
+ if (in_array($idSite, $this->getSitesIdWithViewAccess())) {
+ return 'view';
+ }
+
+ return 'noaccess';
+ }
+
+ /**
+ * Returns the capabilities the current user has for a given site.
+ *
+ * @param int $idSite The site to check.
+ * @return string[] The capabilities the user has.
+ */
+ public function getCapabilitiesForSite($idSite)
+ {
+ $result = [];
+ foreach ($this->capabilityProvider->getAllCapabilityIds() as $capabilityId) {
+ if (empty($this->idsitesByAccess[$capabilityId])) {
+ continue;
+ }
+
+ if (in_array($idSite, $this->idsitesByAccess[$capabilityId])) {
+ $result[] = $capabilityId;
+ }
+ }
+ return $result;
+ }
}
/**
diff --git a/core/Access/CapabilitiesProvider.php b/core/Access/CapabilitiesProvider.php
index 358782eaca..6ed19d69d7 100644
--- a/core/Access/CapabilitiesProvider.php
+++ b/core/Access/CapabilitiesProvider.php
@@ -62,6 +62,8 @@ class CapabilitiesProvider
$capabilities = array_values($capabilities);
+ $this->checkCapabilityIds($capabilities);
+
$cache->save($cacheId, $capabilities);
return $capabilities;
}
@@ -105,4 +107,17 @@ class CapabilitiesProvider
throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", implode(", ", $capabilities)));
}
}
+
+ /**
+ * @param Capability[] $capabilities
+ */
+ private function checkCapabilityIds($capabilities)
+ {
+ foreach ($capabilities as $capability) {
+ $id = $capability->getId();
+ if (preg_match('/[^a-zA-Z0-9_-]/', $id)) {
+ throw new \Exception("Capability with invalid ID found: '$id'. Valid characters are 'a-zA-Z0-9_-'.");
+ }
+ }
+ }
}
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index 6d9cc141bd..19c8c7bad8 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -7,6 +7,7 @@
*
*/
namespace Piwik\Plugins\CoreHome;
+
use Piwik\Columns\ComputedMetricFactory;
use Piwik\Columns\MetricsList;
use Piwik\IP;
@@ -126,6 +127,7 @@ class CoreHome extends \Piwik\Plugin
$stylesheets[] = "plugins/CoreHome/angularjs/period-date-picker/period-date-picker.component.less";
$stylesheets[] = "plugins/CoreHome/angularjs/period-selector/period-selector.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/multipairfield/multipairfield.directive.less";
+ $stylesheets[] = "plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.less";
$stylesheets[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.less";
}
@@ -272,6 +274,8 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreHome/angularjs/multipairfield/multipairfield.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/multipairfield/multipairfield.controller.js";
+ $jsFiles[] = "plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.js";
+
$jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.controller.js";
@@ -284,7 +288,6 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreAdminHome/angularjs/trackingcode/imagetrackingcode.controller.js";
$jsFiles[] = "plugins/CoreAdminHome/angularjs/archiving/archiving.controller.js";
-
// we have to load these CorePluginsAdmin files here. If we loaded them in CorePluginsAdmin,
// there would be JS errors as CorePluginsAdmin is loaded first. Meaning it is loaded before
// any angular JS file is loaded etc.
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js
index 0f77fd7dbb..7a9f729963 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js
@@ -94,6 +94,7 @@ var hasBlockedContent = false;
function onSuccess(response)
{
+ var headers = response.headers;
response = response.data;
if (!angular.isDefined(response) || response === null) {
@@ -105,7 +106,7 @@ var hasBlockedContent = false;
return $q.reject(response.message || null);
} else {
- return response;
+ return options.includeHeaders ? { headers: headers, response: response } : response;
}
}
diff --git a/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.js b/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.js
new file mode 100644
index 0000000000..babb8b49b6
--- /dev/null
+++ b/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.js
@@ -0,0 +1,56 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * A materializecss dropdown menu that supports submenus.
+ *
+ * To use a submenu, just use this directive within another dropdown.
+ *
+ * Note: if submenus are used, then dropdowns will never scroll.
+ *
+ * Usage:
+ * <a class='dropdown-trigger btn' href='' data-activates='mymenu' piwik-dropdown-menu>Menu</a>
+ * <ul id='mymenu' class='dropdown-content'>
+ * <li>
+ * <a class='dropdown-trigger' data-activates="mysubmenu" piwik-dropdown-menu>Submenu</a>
+ * <ul id="mysubmenu" class="dropdown-content">
+ * <li>Submenu Item</li>
+ * </ul>
+ * </li>
+ * <li>
+ * <a href="">Another item</a>
+ * </li>
+ * </ul>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikDropdownMenu', piwikDropdownMenu);
+
+ piwikDropdownMenu.$inject = ['$timeout'];
+
+ function piwikDropdownMenu($timeout){
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+ var options = {};
+
+ var isSubmenu = !! element.parent().closest('.dropdown-content').length;
+ if (isSubmenu) {
+ options = { hover: true };
+ element.addClass('submenu');
+ angular.element('#' + attrs.activates).addClass('submenu-dropdown-content');
+
+ // if a submenu is used, the dropdown will never scroll
+ element.parents('.dropdown-content').addClass('submenu-container');
+ }
+
+ $timeout(function () {
+ element.dropdown(options);
+ });
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less b/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less
new file mode 100644
index 0000000000..92dda6cf22
--- /dev/null
+++ b/plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less
@@ -0,0 +1,27 @@
+[piwik-dropdown-menu] {
+ position: relative;
+
+ &::after {
+ content: "â–¼";
+ font-size: .7em;
+ position: absolute;
+ right: 1em;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ &.submenu::after {
+ float: right;
+ content: "â–º";
+ color: @color-black-piwik;
+ font-size: .6em;
+ }
+}
+
+.submenu-dropdown-content.dropdown-content {
+ left: 100% !important;
+}
+
+.submenu-container.dropdown-content {
+ overflow:visible; // required for submenus to display
+} \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/notification/notification.directive.js b/plugins/CoreHome/angularjs/notification/notification.directive.js
index b05eba463f..3be9273ca8 100644
--- a/plugins/CoreHome/angularjs/notification/notification.directive.js
+++ b/plugins/CoreHome/angularjs/notification/notification.directive.js
@@ -37,13 +37,16 @@
// HTML of the node that uses the directive.
context: '@?',
type: '@?',
- noclear: '@?'
+ noclear: '@?',
+ toastLength: '@?'
},
transclude: true,
templateUrl: 'plugins/CoreHome/angularjs/notification/notification.directive.html?cb=' + piwik.cacheBuster,
controller: 'NotificationController',
controllerAs: 'notification',
link: function (scope, element) {
+ scope.toastLength = scope.toastLength || 12 * 1000;
+
if (scope.notificationId) {
closeExistingNotificationHavingSameIdIfNeeded(scope.notificationId, element);
}
@@ -70,7 +73,7 @@
element.fadeOut('slow', function() {
element.remove();
});
- }, 12 * 1000);
+ }, scope.toastLength);
}
function addCloseEvent() {
diff --git a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.html b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.html
index 463a723720..acac9daffc 100644
--- a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.html
+++ b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.html
@@ -21,7 +21,9 @@
class="title" tabindex="4">
<span class="icon icon-arrow-bottom"
ng-class="{'iconHidden': model.isLoading, 'collapsed': !view.showSitesList}"></span>
- <span><span ng-bind-html="selectedSite.name || model.firstSiteName">?</span>
+ <span>
+ <span ng-bind-html="selectedSite.name || model.firstSiteName" ng-if="selectedSite.name || !placeholder">?</span>
+ <span ng-if="!selectedSite.name && placeholder" class="placeholder">{{ placeholder }}</span>
</span>
</a>
diff --git a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.js b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.js
index 75dbf1e405..9c4d887c6b 100644
--- a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.js
+++ b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.js
@@ -50,7 +50,8 @@
onlySitesWithAdminAccess: '=',
inputName: '@name',
allSitesText: '@',
- allSitesLocation: '@'
+ allSitesLocation: '@',
+ placeholder: '@'
},
require: "?ngModel",
templateUrl: 'plugins/CoreHome/angularjs/siteselector/siteselector.directive.html?cb=' + piwik.cacheBuster,
diff --git a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.less b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.less
index 41916a205d..36f79a490f 100644
--- a/plugins/CoreHome/angularjs/siteselector/siteselector.directive.less
+++ b/plugins/CoreHome/angularjs/siteselector/siteselector.directive.less
@@ -7,6 +7,11 @@
.icon.collapsed.iconHidden {
visibility: visible;
}
+
+ span.placeholder {
+ color: #9e9e9e;
+ font-style: italic;
+ }
}
.dropdown {
width: 210px;
diff --git a/plugins/CoreHome/javascripts/notification.js b/plugins/CoreHome/javascripts/notification.js
index b5dbceaee6..f0563c203e 100644
--- a/plugins/CoreHome/javascripts/notification.js
+++ b/plugins/CoreHome/javascripts/notification.js
@@ -41,14 +41,8 @@
* wherever you want.
*/
Notification.prototype.show = function (message, options) {
- if (!message) {
- throw new Error('No message given, cannot display notification');
- }
- if (options && !$.isPlainObject(options)) {
- throw new Error('Options has the wrong format, cannot display notification');
- } else if (!options) {
- options = {};
- }
+ checkMessage(message);
+ options = checkOptions(options);
var template = generateNotificationHtmlMarkup(options, message);
this.$node = placeNotification(template, options);
@@ -70,6 +64,59 @@
}
};
+ /**
+ * Shows a notification at a certain point with a quick upwards animation.
+ *
+ * TODO: if the materializecss version matomo uses is updated, should use their toasts.
+ *
+ * @type {Notification}
+ * @param {string} message The actual message that will be displayed. Must be set.
+ * @param {Object} options
+ * @param {string} options.placeat Where to place the notification. Required.
+ * @param {string} [options.id] Only needed for persistent notifications. The id will be sent to the
+ * frontend once the user closes the notifications. The notification has to
+ * be registered/notified under this name
+ * @param {string} [options.title] The title of the notification. For instance the plugin name.
+ * @param {string} [options.context=warning] Context of the notification: 'info', 'warning', 'success' or
+ * 'error'
+ * @param {string} [options.type=transient] The type of the notification: Either 'toast' or 'transitent'
+ * @param {bool} [options.noclear=false] If set, the close icon is not displayed.
+ * @param {object} [options.style] Optional style/css dictionary. For instance {'display': 'inline-block'}
+ */
+ Notification.prototype.toast = function (message, options) {
+ checkMessage(message);
+ options = checkOptions(options);
+
+ var $placeat = $(options.placeat);
+ if (!$placeat.length) {
+ throw new Error("A valid selector is required for the placeat option when using Notification.toast().");
+ }
+
+ var $template = $(generateNotificationHtmlMarkup(options, message)).hide();
+ $('body').append($template);
+
+ compileNotification($template);
+
+ $template.css({
+ position: 'absolute',
+ left: $placeat.offset().left,
+ top: $placeat.offset().top
+ });
+ setTimeout(function () {
+ $template.animate(
+ {
+ top: $placeat.offset().top - $template.height()
+ },
+ {
+ duration: 300,
+ start: function () {
+ $template.show();
+ }
+ }
+ );
+ });
+ };
+
exports.Notification = Notification;
function generateNotificationHtmlMarkup(options, message) {
@@ -78,7 +125,9 @@
title: 'notification-title',
context: 'context',
type: 'type',
- noclear: 'noclear'
+ noclear: 'noclear',
+ class: 'class',
+ toastLength: 'toast-length'
},
html = '<div piwik-notification';
@@ -95,13 +144,17 @@
return html;
}
+ function compileNotification($node) {
+ angular.element(document).injector().invoke(function ($compile, $rootScope) {
+ $compile($node)($rootScope.$new(true));
+ });
+ }
+
function placeNotification(template, options) {
var $notificationNode = $(template);
// compile the template in angular
- angular.element(document).injector().invoke(function ($compile, $rootScope) {
- $compile($notificationNode)($rootScope.$new(true));
- });
+ compileNotification($notificationNode);
if (options.style) {
$notificationNode.css(options.style);
@@ -133,4 +186,19 @@
return $notificationNode;
}
+ function checkMessage(message) {
+ if (!message) {
+ throw new Error('No message given, cannot display notification');
+ }
+ }
+
+ function checkOptions(options) {
+ if (options && !$.isPlainObject(options)) {
+ throw new Error('Options has the wrong format, cannot display notification');
+ } else if (!options) {
+ options = {};
+ }
+ return options;
+ }
+
})(jQuery, require); \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/angularjs/field/field.directive.js b/plugins/CorePluginsAdmin/angularjs/field/field.directive.js
index 4c2a3564f8..e64179fc43 100644
--- a/plugins/CorePluginsAdmin/angularjs/field/field.directive.js
+++ b/plugins/CorePluginsAdmin/angularjs/field/field.directive.js
@@ -9,7 +9,7 @@
* Usage:
* <div piwik-field>
*
- * eg <div piwik-field ui-control="select"
+ * eg <div piwik-field uicontrol="select"
* title="{{ 'SitesManager_Timezone'|translate }}"
* value="site.timezone"
* options="timezones"
diff --git a/plugins/CorePluginsAdmin/angularjs/form-field/field-select.html b/plugins/CorePluginsAdmin/angularjs/form-field/field-select.html
index a9018bbf98..36bedbdc25 100644
--- a/plugins/CorePluginsAdmin/angularjs/form-field/field-select.html
+++ b/plugins/CorePluginsAdmin/angularjs/form-field/field-select.html
@@ -1,8 +1,10 @@
<div>
<select name="{{ formField.name }}"
ng-model="formField.value"
- ng-options="t.key as t.value group by t.group for t in formField.availableOptions"
- piwik-attributes="{{formField.uiControlAttributes}}">
+ ng-options="t.key as t.value group by t.group disable when t.disabled for t in formField.availableOptions"
+ piwik-attributes="{{formField.uiControlAttributes}}"
+ ng-click="onShowSelect()"
+ >
</select>
<label for="{{ formField.name }}" ng-bind-html="formField.title"></label>
</div>
diff --git a/plugins/CorePluginsAdmin/angularjs/form-field/field-site.html b/plugins/CorePluginsAdmin/angularjs/form-field/field-site.html
index 5fa647685e..7a1e6729a3 100644
--- a/plugins/CorePluginsAdmin/angularjs/form-field/field-site.html
+++ b/plugins/CorePluginsAdmin/angularjs/form-field/field-site.html
@@ -4,8 +4,11 @@
class="sites_autocomplete"
ng-model="formField.value"
id="{{ formField.name }}"
- show-all-sites-item="false"
+ show-all-sites-item="formField.uiControlAttributes.showAllSitesItem || false"
switch-site-on-select="false"
show-selected-site="true"
- piwik-attributes="{{formField.uiControlAttributes}}"></div>
+ only-sites-with-admin-access="formField.uiControlAttributes.onlySitesWithAdminAccess || false"
+ placeholder="{{ formField.uiControlAttributes.placeholder }}"
+ piwik-attributes="{{formField.uiControlAttributes}}"
+ ></div>
</div>
diff --git a/plugins/CorePluginsAdmin/angularjs/form-field/form-field.directive.js b/plugins/CorePluginsAdmin/angularjs/form-field/form-field.directive.js
index 6774fda081..5637c88902 100644
--- a/plugins/CorePluginsAdmin/angularjs/form-field/form-field.directive.js
+++ b/plugins/CorePluginsAdmin/angularjs/form-field/form-field.directive.js
@@ -16,6 +16,30 @@
function piwikFormField(piwik, $timeout){
+ function initMaterialSelect($select, placeholder) {
+ $select.material_select();
+
+ // to prevent overlapping selects, when a select is opened, we set the z-index to a high value on focus & remove z-index for all others
+ // NOTE: we can't remove it directly blur since the blur causes the select to overlap, aborting the select click. (a timeout is used
+ // to make sure the z-index is removed however, in case a non-select dropdown is displayed over it)
+ $select.closest('.select-wrapper').find('input.select-dropdown')
+ .focus(function () {
+ $('.select-wrapper').css('z-index', '');
+ $(this).closest('.select-wrapper').css('z-index', 999);
+ }).blur(function () {
+ var self = this;
+ setTimeout(function () {
+ $(self).closest('.select-wrapper').css('z-index', '');
+ }, 250);
+ });
+
+ // add placeholder to input
+ if (placeholder) {
+ var $materialInput = $select.closest('.select-wrapper').find('input');
+ $materialInput.attr('placeholder', placeholder);
+ }
+ }
+
function syncMultiCheckboxKeysWithFieldValue(field)
{
angular.forEach(field.availableOptions, function (option, index) {
@@ -73,12 +97,12 @@
if (isSelectControl(field)) {
var $select = element.find('select');
- $select.material_select();
+ initMaterialSelect($select, field.uiControlAttributes.placeholder);
scope.$watch('formField.value', function (val, oldVal) {
if (val !== oldVal) {
$timeout(function () {
- $select.material_select();
+ initMaterialSelect($select, field.uiControlAttributes.placeholder);
});
}
});
@@ -86,7 +110,7 @@
scope.$watch('formField.uiControlAttributes.disabled', function (val, oldVal) {
if (val !== oldVal) {
$timeout(function () {
- $select.material_select();
+ initMaterialSelect($select, field.uiControlAttributes.placeholder);
});
}
});
@@ -342,6 +366,11 @@
// availableValues and in the watch change availableValues could trigger lots of more watch events
field.availableOptions = formatAvailableValues(field);
+ // for selects w/ a placeholder, add an option to unset the select
+ if (field.uiControl === 'select' && field.uiControlAttributes.placeholder) {
+ field.availableOptions.splice(0, 0, { key: '', value: '' });
+ }
+
field.defaultValuePretty = formatPrettyDefaultValue(defaultValue, field.availableOptions);
field.showField = true;
@@ -395,7 +424,7 @@
if (isSelectControl(scope.formField)) {
$timeout(function () {
- element.find('select').material_select();
+ initMaterialSelect(element.find('select'), field.uiControlAttributes.placeholder);
});
}
}
@@ -403,7 +432,6 @@
scope.templateLoaded = function () {
$timeout(whenRendered(scope, element, inlineHelpNode));
};
-
};
}
};
diff --git a/plugins/Morpheus/stylesheets/general/_form.less b/plugins/Morpheus/stylesheets/general/_form.less
index 9a21d76ed4..3ce2f7525e 100644
--- a/plugins/Morpheus/stylesheets/general/_form.less
+++ b/plugins/Morpheus/stylesheets/general/_form.less
@@ -50,3 +50,11 @@
padding: 10px 0;
font-size: 12px;
}
+
+// material select additions
+ul.select-dropdown li.disabled span {
+ color: #9e9e9e !important;
+ &:hover {
+ background-color: #fff;
+ }
+} \ No newline at end of file
diff --git a/plugins/SitesManager/Model.php b/plugins/SitesManager/Model.php
index 5e69d308e7..8cc3bb7f9c 100644
--- a/plugins/SitesManager/Model.php
+++ b/plugins/SitesManager/Model.php
@@ -395,7 +395,7 @@ class Model
}
$ids_str .= (int) $id_val;
- $bind = array('%' . $pattern . '%', 'http%' . $pattern . '%', '%' . $pattern . '%');
+ $bind = self::getPatternMatchSqlBind($pattern);
// Also match the idsite
$where = '';
@@ -406,9 +406,7 @@ class Model
$query = "SELECT *
FROM " . $this->table . " s
- WHERE ( s.name like ?
- OR s.main_url like ?
- OR s.`group` like ?
+ WHERE ( " . self::getPatternMatchSqlQuery('s') . "
$where )
AND idsite in ($ids_str)";
@@ -422,6 +420,16 @@ class Model
return $sites;
}
+ public static function getPatternMatchSqlQuery($table)
+ {
+ return "($table.name like ? OR $table.main_url like ? OR $table.group like ?)";
+ }
+
+ public static function getPatternMatchSqlBind($pattern)
+ {
+ return array('%' . $pattern . '%', 'http%' . $pattern . '%', '%' . $pattern . '%');
+ }
+
/**
* Delete all the alias URLs for the given idSite.
*/
diff --git a/plugins/UsersManager/API.php b/plugins/UsersManager/API.php
index 241595f571..20062acbce 100644
--- a/plugins/UsersManager/API.php
+++ b/plugins/UsersManager/API.php
@@ -10,11 +10,15 @@ namespace Piwik\Plugins\UsersManager;
use Exception;
use Piwik\Access;
+use Piwik\Access\CapabilitiesProvider;
+use Piwik\Access\RolesProvider;
use Piwik\Auth\Password;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Date;
+use Piwik\Metrics\Formatter;
+use Piwik\NoAccessException;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\SettingsPiwik;
@@ -53,6 +57,11 @@ class API extends \Piwik\Plugin\API
private $userFilter;
/**
+ * @var Access
+ */
+ private $access;
+
+ /**
* @var Access\RolesProvider
*/
private $roleProvider;
@@ -67,20 +76,14 @@ class API extends \Piwik\Plugin\API
private static $instance = null;
- public function __construct(Model $model, UserAccessFilter $filter, Password $password, Access\RolesProvider $roleProvider = null, Access\CapabilitiesProvider $capabilityProvider = null)
+ public function __construct(Model $model, UserAccessFilter $filter, Password $password, Access $access = null, Access\RolesProvider $roleProvider = null, Access\CapabilitiesProvider $capabilityProvider = null)
{
$this->model = $model;
$this->userFilter = $filter;
$this->password = $password;
-
- if (!isset($roleProvider)) {
- $roleProvider = StaticContainer::get('Piwik\Access\RolesProvider');
- }
- if (!isset($capabilityProvider)) {
- $capabilityProvider = StaticContainer::get('Piwik\Access\CapabilitiesProvider');
- }
- $this->roleProvider = $roleProvider;
- $this->capabilityProvider = $capabilityProvider;
+ $this->access = $access ?: StaticContainer::get(Access::class);
+ $this->roleProvider = $roleProvider ?: StaticContainer::get(RolesProvider::class);
+ $this->capabilityProvider = $capabilityProvider ?: StaticContainer::get(CapabilitiesProvider::class);
}
/**
@@ -265,6 +268,72 @@ class API extends \Piwik\Plugin\API
}
/**
+ * Returns all users with their role for $idSite.
+ *
+ * @param int $idSite
+ * @param int|null $limit
+ * @param int|null $offset
+ * @param string|null $filter_search text to search for in the user's login, email and alias (if any)
+ * @param string|null $filter_access only select users with this access to $idSite. can be 'noaccess', 'some', 'view', 'admin', 'superuser'
+ * Filtering by 'superuser' is only allowed for other superusers.
+ * @return array
+ */
+ public function getUsersPlusRole($idSite, $limit = null, $offset = 0, $filter_search = null, $filter_access = null)
+ {
+ if (!$this->isUserHasAdminAccessTo($idSite)) {
+ // if the user is not an admin to $idSite, they can only see their own user
+ if ($offset > 1) {
+ Common::sendHeader('X-Matomo-Total-Results: 1');
+ return [];
+ }
+
+ $user = $this->model->getUser($this->access->getLogin());
+ $user['role'] = $this->access->getRoleForSite($idSite);
+ $user['capabilities'] = $this->access->getCapabilitiesForSite($idSite);
+ $users = [$user];
+ $totalResults = 1;
+ } else {
+ // if the current user is not the superuser, only select users that have access to a site this user
+ // has admin access to
+ $loginsToLimit = null;
+ if (!Piwik::hasUserSuperUserAccess()) {
+ $adminIdSites = Access::getInstance()->getSitesIdWithAdminAccess();
+ if (empty($adminIdSites)) { // sanity check
+ throw new \Exception("The current admin user does not have access to any sites.");
+ }
+
+ $loginsToLimit = $this->model->getUsersWithAccessToSites($adminIdSites);
+ }
+
+ list($users, $totalResults) = $this->model->getUsersWithRole($idSite, $limit, $offset, $filter_search, $filter_access, $loginsToLimit);
+
+ foreach ($users as &$user) {
+ $user['superuser_access'] = $user['superuser_access'] == 1;
+ if ($user['superuser_access']) {
+ $user['role'] = 'superuser';
+ $user['capabilities'] = [];
+ } else {
+ list($user['role'], $user['capabilities']) = $this->getRoleAndCapabilitiesFromAccess($user['access']);
+ $user['role'] = empty($user['role']) ? 'noaccess' : reset($user['role']);
+ }
+
+ unset($user['access']);
+ }
+ }
+
+ $users = $this->enrichUsers($users);
+ $users = $this->enrichUsersWithLastSeen($users);
+ $users = $this->removeUserInfoForNonSuperUsers($users);
+
+ foreach ($users as &$user) {
+ unset($user['password']);
+ }
+
+ Common::sendHeader('X-Matomo-Total-Results: ' . $totalResults);
+ return $users;
+ }
+
+ /**
* Returns the list of all the users
*
* @param string $userLogins Comma separated list of users to select. If not specified, will return all users
@@ -284,11 +353,7 @@ class API extends \Piwik\Plugin\API
$users = $this->enrichUsers($users);
// Non Super user can only access login & alias
- if (!Piwik::hasUserSuperUserAccess()) {
- foreach ($users as &$user) {
- $user = array('login' => $user['login'], 'alias' => $user['alias']);
- }
- }
+ $users = $this->removeUserInfoForNonSuperUsers($users);
return $users;
}
@@ -336,12 +401,16 @@ class API extends \Piwik\Plugin\API
private function checkAccessType($access)
{
+ $access = (array) $access;
+
$roles = $this->roleProvider->getAllRoleIds();
$capabilities = $this->capabilityProvider->getAllCapabilityIds();
$list = array_merge($roles, $capabilities);
- if (!in_array($access, $list, true)) {
- throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", implode(", ", $list)));
+ foreach ($access as $entry) {
+ if (!in_array($entry, $list, true)) {
+ throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", implode(", ", $list), $entry));
+ }
}
}
@@ -407,7 +476,6 @@ class API extends \Piwik\Plugin\API
{
Piwik::checkUserHasSuperUserAccess();
$this->checkUserExists($userLogin);
-
// Super users have 'admin' access for every site
if (Piwik::hasTheUserSuperUserAccess($userLogin)) {
$return = array();
@@ -418,15 +486,66 @@ class API extends \Piwik\Plugin\API
'site' => $site['idsite'],
'access' => 'admin'
);
-
}
return $return;
}
-
return $this->model->getSitesAccessFromUser($userLogin);
}
/**
+ * For each website ID, returns the access level of the given $userLogin (if the user is not a superuser).
+ * If the user doesn't have any access to a website ('noaccess'),
+ * this website will not be in the returned array.
+ * If the user doesn't have any access, the returned array will be an empty array.
+ *
+ * @param string $userLogin User that has to be valid
+ *
+ * @param int|null $limit
+ * @param int|null $offset
+ * @param string|null $filter_search text to search for in site name, URLs, or group.
+ * @param string|null $filter_access access level to select for, can be 'some', 'view' or 'admin' (by default 'some')
+ * @return array The returned array has the format
+ * array(
+ * ['idsite' => 1, 'site_name' => 'the site', 'access' => 'admin'],
+ * ['idsite' => 2, 'site_name' => 'the other site', 'access' => 'view'],
+ * ...
+ * )
+ * @throws Exception
+ */
+ public function getSitesAccessForUser($userLogin, $limit = null, $offset = 0, $filter_search = null, $filter_access = null)
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+ $this->checkUserExists($userLogin);
+
+ if (Piwik::hasTheUserSuperUserAccess($userLogin)) {
+ throw new \Exception("This method should not be used with superusers.");
+ }
+
+ $idSites = null;
+ if (!Piwik::hasUserSuperUserAccess()) {
+ $idSites = $this->access->getSitesIdWithAdminAccess();
+ if (empty($idSites)) { // sanity check
+ throw new \Exception("The current admin user does not have access to any sites.");
+ }
+ }
+
+ list($sites, $totalResults) = $this->model->getSitesAccessFromUserWithFilters($userLogin, $limit, $offset, $filter_search, $filter_access, $idSites);
+ foreach ($sites as &$siteAccess) {
+ list($siteAccess['role'], $siteAccess['capabilities']) = $this->getRoleAndCapabilitiesFromAccess($siteAccess['access']);
+ $siteAccess['role'] = empty($siteAccess['role']) ? 'noaccess' : reset($siteAccess['role']);
+ unset($siteAccess['access']);
+ }
+
+ $hasAccessToAny = $this->model->getSiteAccessCount($userLogin) > 0;
+
+ Common::sendHeader('X-Matomo-Total-Results: ' . $totalResults);
+ if ($hasAccessToAny) {
+ Common::sendHeader('X-Matomo-Has-Some: 1');
+ }
+ return $sites;
+ }
+
+ /**
* Returns the user information (login, password hash, alias, email, date_registered, etc.)
*
* @param string $userLogin the user login
@@ -510,9 +629,17 @@ class API extends \Piwik\Plugin\API
*
* @exception in case of an invalid parameter
*/
- public function addUser($userLogin, $password, $email, $alias = false, $_isPasswordHashed = false)
+ public function addUser($userLogin, $password, $email, $alias = false, $_isPasswordHashed = false, $initialIdSite = null)
{
- Piwik::checkUserHasSuperUserAccess();
+ Piwik::checkUserHasSomeAdminAccess();
+
+ if (!Piwik::hasUserSuperUserAccess()) {
+ if (empty($initialIdSite)) {
+ throw new \Exception(Piwik::translate("UsersManager_AddUserNoInitialAccessError"));
+ }
+
+ Piwik::checkUserHasAdminAccess($initialIdSite);
+ }
$this->checkLogin($userLogin);
$this->checkEmail($email);
@@ -543,6 +670,10 @@ class API extends \Piwik\Plugin\API
* @param string $userLogin The new user's login handle.
*/
Piwik::postEvent('UsersManager.addUser.end', array($userLogin, $email, $password, $alias));
+
+ if ($initialIdSite) {
+ $this->setUserAccess($userLogin, 'view', $initialIdSite);
+ }
}
/**
@@ -599,6 +730,20 @@ class API extends \Piwik\Plugin\API
return $users;
}
+ private function enrichUsersWithLastSeen($users)
+ {
+ $formatter = new Formatter();
+
+ $lastSeenTimes = LastSeenTimeLogger::getLastSeenTimesForAllUsers();
+ foreach ($users as &$user) {
+ $login = $user['login'];
+ if (isset($lastSeenTimes[$login])) {
+ $user['last_seen'] = $formatter->getPrettyTimeFromSeconds(time() - $lastSeenTimes[$login]);
+ }
+ }
+ return $users;
+ }
+
private function enrichUsers($users)
{
if (!empty($users)) {
@@ -613,6 +758,8 @@ class API extends \Piwik\Plugin\API
{
if (!empty($user)) {
unset($user['token_auth']);
+ unset($user['password']);
+ unset($user['ts_password_modified']);
}
return $user;
@@ -709,11 +856,11 @@ class API extends \Piwik\Plugin\API
}
/**
- * Delete a user and all its access, given its login.
+ * Delete one or more users and all its access, given its login.
*
- * @param string $userLogin the user login.
+ * @param string $userLogin the user login(s).
*
- * @throws Exception if the user doesn't exist
+ * @throws Exception if the user doesn't exist or if deleting the users would leave no superusers.
*
* @return bool true on success
*/
@@ -722,9 +869,7 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasSuperUserAccess();
$this->checkUserIsNotAnonymous($userLogin);
- if (!$this->userExists($userLogin)) {
- throw new Exception(Piwik::translate("UsersManager_ExceptionDeleteDoesNotExist", $userLogin));
- }
+ $this->checkUserExist($userLogin);
if ($this->isUserTheOnlyUserHavingSuperUserAccess($userLogin)) {
$message = Piwik::translate("UsersManager_ExceptionDeleteOnlyUserWithSuperUserAccess", $userLogin)
@@ -807,17 +952,21 @@ class API extends \Piwik\Plugin\API
* May also be an array to sent additional capabilities
* @param int|array $idSites The array of idSites on which to apply the access level for the user.
* 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.
- *
* @throws Exception if the user doesn't exist
* @throws Exception if the access parameter doesn't have a correct value
* @throws Exception if any of the given website ID doesn't exist
*/
public function setUserAccess($userLogin, $access, $idSites)
{
+ if ($access != 'noaccess') {
+ $this->checkAccessType($access);
+ }
+
$idSites = $this->getIdSitesCheckAdminAccess($idSites);
if ($userLogin === 'anonymous' &&
- (is_array($access) || !in_array($access, array('view', 'noaccess'), true))) {
+ (is_array($access) || !in_array($access, array('view', 'noaccess'), true))
+ ) {
throw new Exception(Piwik::translate("UsersManager_ExceptionAnonymousAccessNotPossible", array('noaccess', 'view')));
}
@@ -826,15 +975,7 @@ class API extends \Piwik\Plugin\API
if (is_array($access)) {
// we require one role, and optionally multiple capabilties
- $roles = array();
- foreach ($access as $entry) {
- if ($this->roleProvider->isValidRole($entry)) {
- $roles[] = $entry;
- } else {
- $this->checkAccessType($entry);
- $capabilities[] = $entry;
- }
- }
+ list($roles, $capabilities) = $this->getRoleAndCapabilitiesFromAccess($access);
if (count($roles) < 1) {
$ids = implode(', ', $this->roleProvider->getAllRoleIds());
@@ -854,8 +995,8 @@ class API extends \Piwik\Plugin\API
}
}
- $this->checkUserExists($userLogin);
- $this->checkUserHasNotSuperUserAccess($userLogin);
+ $this->checkUserExist($userLogin);
+ $this->checkUsersHasNotSuperUserAccess($userLogin);
$this->model->deleteUserAccess($userLogin, $idSites);
@@ -897,7 +1038,7 @@ class API extends \Piwik\Plugin\API
}
$this->checkUserExists($userLogin);
- $this->checkUserHasNotSuperUserAccess($userLogin);
+ $this->checkUsersHasNotSuperUserAccess([$userLogin]);
if (!is_array($capabilities)){
$capabilities = array($capabilities);
@@ -1054,18 +1195,37 @@ class API extends \Piwik\Plugin\API
}
}
- private function checkUserHasNotSuperUserAccess($userLogin)
+ private function checkUsersHasNotSuperUserAccess($userLogins)
{
- if (Piwik::hasTheUserSuperUserAccess($userLogin)) {
- throw new Exception(Piwik::translate("UsersManager_ExceptionSuperUserAccess"));
+ $userLogins = (array) $userLogins;
+ $superusers = $this->getUsersHavingSuperUserAccess();
+ $superusers = array_column($superusers, null, 'login');
+
+ foreach ($userLogins as $userLogin) {
+ if (isset($superusers[$userLogin])) {
+ throw new Exception(Piwik::translate("UsersManager_ExceptionUserHasSuperUserAccess", $userLogin));
+ }
}
}
+ /**
+ * @param string|string[] $userLogin
+ * @return bool
+ */
private function isUserTheOnlyUserHavingSuperUserAccess($userLogin)
{
- $superUsers = $this->getUsersHavingSuperUserAccess();
+ if (!is_array($userLogin)) {
+ $userLogin = [$userLogin];
+ }
+
+ $superusers = $this->getUsersHavingSuperUserAccess();
+ $superusersByLogin = array_column($superusers, null, 'login');
+
+ foreach ($userLogin as $login) {
+ unset($superusersByLogin[$login]);
+ }
- return 1 >= count($superUsers) && Piwik::hasTheUserSuperUserAccess($userLogin);
+ return empty($superusersByLogin);
}
/**
@@ -1104,4 +1264,59 @@ class API extends \Piwik\Plugin\API
return $user['token_auth'];
}
+
+ private function removeUserInfoForNonSuperUsers($users)
+ {
+ if (!Piwik::hasUserSuperUserAccess()) {
+ foreach ($users as $key => $user) {
+ $newUser = array('login' => $user['login'], 'alias' => $user['alias']);
+ if (isset($user['role'])) {
+ $newUser['role'] = $user['role'] == 'superuser' ? 'admin' : $user['role'];
+ }
+ if (isset($user['capabilities'])) {
+ $newUser['capabilities'] = $user['capabilities'];
+ }
+ $users[$key] = $newUser;
+ }
+ }
+ return $users;
+ }
+
+ private function isUserHasAdminAccessTo($idSite)
+ {
+ try {
+ Piwik::checkUserHasAdminAccess([$idSite]);
+ return true;
+ } catch (NoAccessException $ex) {
+ return false;
+ }
+ }
+
+ private function checkUserExist($userLogin)
+ {
+ $userExists = $this->model->userExists($userLogin);
+ if (!$userExists) {
+ throw new Exception(Piwik::translate("UsersManager_ExceptionUserDoesNotExist", $userLogin));
+ }
+ }
+
+ private function getRoleAndCapabilitiesFromAccess($access)
+ {
+ $roles = [];
+ $capabilities = [];
+
+ foreach ($access as $entry) {
+ if (empty($entry)) {
+ continue;
+ }
+
+ if ($this->roleProvider->isValidRole($entry)) {
+ $roles[] = $entry;
+ } else {
+ $this->checkAccessType($entry);
+ $capabilities[] = $entry;
+ }
+ }
+ return [$roles, $capabilities];
+ }
}
diff --git a/plugins/UsersManager/Controller.php b/plugins/UsersManager/Controller.php
index 6b59d05437..310003c2d2 100644
--- a/plugins/UsersManager/Controller.php
+++ b/plugins/UsersManager/Controller.php
@@ -9,6 +9,7 @@
namespace Piwik\Plugins\UsersManager;
use Exception;
+use Piwik\Access;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
@@ -50,7 +51,7 @@ class Controller extends ControllerAdmin
/**
* The "Manage Users and Permissions" Admin UI screen
*/
- function index()
+ public function index()
{
Piwik::checkUserIsNotAnonymous();
Piwik::checkUserHasSomeAdminAccess();
@@ -65,104 +66,37 @@ class Controller extends ControllerAdmin
$idSiteSelected = Common::getRequestVar('idSite', $defaultWebsiteId);
}
- if ($idSiteSelected === 'all') {
- $usersAccessByWebsite = array();
- $defaultReportSiteName = $this->translator->translate('UsersManager_ApplyToAllWebsites');
- } else {
-
- if (!Piwik::isUserHasAdminAccess($idSiteSelected) && count($IdSitesAdmin) > 0) {
- // make sure to show a website where user actually has admin access
- $idSiteSelected = $IdSitesAdmin[0];
- }
-
- $defaultReportSiteName = Site::getNameFor($idSiteSelected);
- try {
- $usersAccessByWebsite = Request::processRequest('UsersManager.getUsersAccessFromSite', array('idSite' => $idSiteSelected));
- } catch (NoAccessException $e) {
- return $this->noAdminAccessToWebsite($idSiteSelected, $defaultReportSiteName, $e->getMessage());
- }
- }
-
- // we don't want to display the user currently logged so that the user can't change his settings from admin to view...
- $currentlyLogged = Piwik::getCurrentUserLogin();
- $usersLogin = Request::processRequest('UsersManager.getUsersLogin');
- foreach ($usersLogin as $login) {
- if (!isset($usersAccessByWebsite[$login])) {
- $usersAccessByWebsite[$login] = 'noaccess';
- }
- }
- unset($usersAccessByWebsite[$currentlyLogged]);
-
- // $usersAccessByWebsite is not supposed to contain unexistant logins, but it does when upgrading from some old Piwik version
- foreach ($usersAccessByWebsite as $login => $access) {
- if (!in_array($login, $usersLogin)) {
- unset($usersAccessByWebsite[$login]);
- continue;
- }
+ if (!Piwik::isUserHasAdminAccess($idSiteSelected) && count($IdSitesAdmin) > 0) {
+ // make sure to show a website where user actually has admin access
+ $idSiteSelected = $IdSitesAdmin[0];
}
- ksort($usersAccessByWebsite);
-
- $users = array();
- $superUsers = array();
- $usersAliasByLogin = array();
-
- $formatter = new Formatter();
-
- if (Piwik::isUserHasSomeAdminAccess()) {
- $view->showLastSeen = true;
+ $defaultReportSiteName = Site::getNameFor($idSiteSelected);
- $users = Request::processRequest('UsersManager.getUsers');
- foreach ($users as $index => $user) {
- $usersAliasByLogin[$user['login']] = $user['alias'];
-
- $lastSeen = LastSeenTimeLogger::getLastSeenTimeForUser($user['login']);
- $users[$index]['last_seen'] = $lastSeen == 0
- ? false : $formatter->getPrettyTimeFromSeconds(time() - $lastSeen);
- }
-
- if (Piwik::hasUserSuperUserAccess()) {
- foreach ($users as $user) {
- if ($user['superuser_access']) {
- $superUsers[] = $user['login'];
- }
- }
- }
- }
-
- $view->hasOnlyAdminAccess = Piwik::isUserHasSomeAdminAccess() && !Piwik::hasUserSuperUserAccess();
- $view->anonymousHasViewAccess = $this->hasAnonymousUserViewAccess($usersAccessByWebsite);
$view->idSiteSelected = $idSiteSelected;
$view->defaultReportSiteName = $defaultReportSiteName;
- $view->users = $users;
- $view->superUserLogins = $superUsers;
- $view->usersAliasByLogin = $usersAliasByLogin;
- $view->usersCount = count($users) - 1;
- $view->usersAccessByWebsite = $usersAccessByWebsite;
-
- $websites = Request::processRequest('SitesManager.getSitesWithAdminAccess');
- uasort($websites, array('Piwik\Plugins\UsersManager\Controller', 'orderByName'));
- $view->websites = $websites;
+ $view->currentUserRole = Piwik::hasUserSuperUserAccess() ? 'superuser' : 'admin';
+ $view->accessLevels = [
+ ['key' => 'noaccess', 'value' => Piwik::translate('UsersManager_PrivNone')],
+ ['key' => 'view', 'value' => Piwik::translate('UsersManager_PrivView')],
+ ['key' => 'write', 'value' => Piwik::translate('UsersManager_PrivWrite')],
+ ['key' => 'admin', 'value' => Piwik::translate('UsersManager_PrivAdmin')],
+ ['key' => 'superuser', 'value' => Piwik::translate('Installation_SuperUser'), 'disabled' => true],
+ ];
+ $view->filterAccessLevels = [
+ ['key' => 'noaccess', 'value' => Piwik::translate('UsersManager_PrivNone')],
+ ['key' => 'some', 'value' => Piwik::translate('UsersManager_AtLeastView')],
+ ['key' => 'view', 'value' => Piwik::translate('UsersManager_PrivView')],
+ ['key' => 'write', 'value' => Piwik::translate('UsersManager_PrivWrite')],
+ ['key' => 'admin', 'value' => Piwik::translate('UsersManager_PrivAdmin')],
+ ['key' => 'superuser', 'value' => Piwik::translate('Installation_SuperUser')],
+ ];
+
$this->setBasicVariablesView($view);
return $view->render();
}
- private function hasAnonymousUserViewAccess($usersAccessByWebsite)
- {
- $anonymousHasViewAccess = false;
-
- foreach ($usersAccessByWebsite as $login => $access) {
- if ($login == 'anonymous'
- && $access != 'noaccess'
- ) {
- $anonymousHasViewAccess = true;
- }
- }
-
- return $anonymousHasViewAccess;
- }
-
/**
* Returns default date for Piwik reports
*
diff --git a/plugins/UsersManager/LastSeenTimeLogger.php b/plugins/UsersManager/LastSeenTimeLogger.php
index a89ebd0018..491805ed82 100644
--- a/plugins/UsersManager/LastSeenTimeLogger.php
+++ b/plugins/UsersManager/LastSeenTimeLogger.php
@@ -68,4 +68,16 @@ class LastSeenTimeLogger
$optionName = self::OPTION_PREFIX . $userName;
return Option::get($optionName);
}
+
+ public static function getLastSeenTimesForAllUsers()
+ {
+ $results = [];
+ foreach (Option::getLike(self::OPTION_PREFIX . '%') as $name => $value) {
+ preg_match('/^' . preg_quote(self::OPTION_PREFIX) . '(.*)$/', $name, $matches);
+ if (isset($matches[1])) {
+ $results[$matches[1]] = $value;
+ }
+ }
+ return $results;
+ }
} \ No newline at end of file
diff --git a/plugins/UsersManager/Model.php b/plugins/UsersManager/Model.php
index f986333bc0..b130459e35 100644
--- a/plugins/UsersManager/Model.php
+++ b/plugins/UsersManager/Model.php
@@ -13,6 +13,9 @@ use Piwik\Common;
use Piwik\Date;
use Piwik\Db;
use Piwik\Piwik;
+use Piwik\Plugins\SitesManager\SitesManager;
+use Piwik\Plugins\UsersManager\Sql\SiteAccessFilter;
+use Piwik\Plugins\UsersManager\Sql\UserTableFilter;
/**
* The UsersManager API lets you Manage Users and their permissions to access specific websites.
@@ -116,9 +119,8 @@ class Model
public function getUsersLoginWithSiteAccess($idSite, $access)
{
$db = $this->getDb();
- $users = $db->fetchAll("SELECT login
- FROM " . Common::prefixTable("access")
- . " WHERE idsite = ? AND access = ?", array($idSite, $access));
+ $users = $db->fetchAll("SELECT login FROM " . Common::prefixTable("access")
+ . " WHERE idsite = ? AND access = ?", array($idSite, $access));
$logins = array();
foreach ($users as $user) {
@@ -148,8 +150,7 @@ class Model
{
$db = $this->getDb();
$users = $db->fetchAll("SELECT idsite,access FROM " . Common::prefixTable("access")
- . " WHERE login = ?", $userLogin);
-
+ . " WHERE login = ?", $userLogin);
$return = array();
foreach ($users as $user) {
$return[] = array(
@@ -157,15 +158,70 @@ class Model
'access' => $user['access'],
);
}
-
return $return;
}
+ public function getSitesAccessFromUserWithFilters($userLogin, $limit = null, $offset = 0, $pattern = null, $access = null, $idSites = null)
+ {
+ $siteAccessFilter = new SiteAccessFilter($userLogin, $pattern, $access, $idSites);
+
+ list($joins, $bind) = $siteAccessFilter->getJoins('a');
+
+ list($where, $whereBind) = $siteAccessFilter->getWhere();
+ $bind = array_merge($bind, $whereBind);
+
+ $limitSql = '';
+ if ($limit) {
+ $limitSql = "LIMIT " . (int)$limit;
+ }
+
+ $offsetSql = '';
+ if ($offset) {
+ $offsetSql = "OFFSET " . (int)$offset;
+ }
+
+ $sql = 'SELECT SQL_CALC_FOUND_ROWS s.idsite as idsite, s.name as site_name, GROUP_CONCAT(a.access SEPARATOR "|") as access
+ FROM ' . Common::prefixTable('access') . " a
+ $joins
+ $where
+ GROUP BY s.idsite
+ ORDER BY s.name ASC, s.idsite ASC
+ $limitSql $offsetSql";
+ $db = $this->getDb();
+
+ $access = $db->fetchAll($sql, $bind);
+ foreach ($access as &$entry) {
+ $entry['access'] = explode('|', $entry['access']);
+ }
+
+ $count = $db->fetchOne("SELECT FOUND_ROWS()");
+
+ return [$access, $count];
+ }
+
+ public function getIdSitesAccessMatching($userLogin, $filter_search = null, $filter_access = null, $idSites = null)
+ {
+ $siteAccessFilter = new SiteAccessFilter($userLogin, $filter_search, $filter_access, $idSites);
+
+ list($joins, $bind) = $siteAccessFilter->getJoins('a');
+
+ list($where, $whereBind) = $siteAccessFilter->getWhere();
+ $bind = array_merge($bind, $whereBind);
+
+ $sql = 'SELECT s.idsite FROM ' . Common::prefixTable('access') . " a $joins $where";
+
+ $db = $this->getDb();
+
+ $sites = $db->fetchAll($sql, $bind);
+ $sites = array_column($sites, 'idsite');
+ return $sites;
+ }
+
public function getUser($userLogin)
{
$db = $this->getDb();
- $matchedUsers = $db->fetchAll("SELECT * FROM " . $this->table . " WHERE login = ?", $userLogin);
+ $matchedUsers = $db->fetchAll("SELECT * FROM {$this->table} WHERE login = ?", $userLogin);
// for BC in 2.15 LTS, if there is a user w/ an exact match to the requested login, return that user.
// this is done since before this change, login was case sensitive. until 3.0, we want to maintain
@@ -285,31 +341,31 @@ class Model
return $count != 0;
}
- public function addUserAccess($userLogin, $access, $idSites)
+ public function removeUserAccess($userLogin, $access, $idSites)
{
$db = $this->getDb();
+ $table = Common::prefixTable("access");
+
foreach ($idSites as $idsite) {
- $db->insert(Common::prefixTable("access"),
- array("idsite" => $idsite,
- "login" => $userLogin,
- "access" => $access)
- );
+ $bind = array($userLogin, $idsite, $access);
+ $db->query("DELETE FROM " . $table . " WHERE login = ? and idsite = ? and access = ?", $bind);
}
}
- public function removeUserAccess($userLogin, $access, $idSites)
+ public function addUserAccess($userLogin, $access, $idSites)
{
$db = $this->getDb();
- $table = Common::prefixTable("access");
-
+ $insertSql = "INSERT INTO " . Common::prefixTable("access") . ' (idsite, login, access) VALUES (?, ?, ?)';
foreach ($idSites as $idsite) {
- $bind = array($userLogin, $idsite, $access);
- $db->query("DELETE FROM " . $table . " WHERE login = ? and idsite = ? and access = ?", $bind);
+ $db->query($insertSql, [$idsite, $userLogin, $access]);
}
}
+ /**
+ * @param string $userLogin
+ */
public function deleteUserOnly($userLogin)
{
$db = $this->getDb();
@@ -321,25 +377,23 @@ class Model
* This event should be used to clean up any data that is related to the now deleted user.
* The **Dashboard** plugin, for example, uses this event to remove the user's dashboards.
*
- * @param string $userLogin The login handle of the deleted user.
+ * @param string $userLogins The login handle of the deleted user.
*/
Piwik::postEvent('UsersManager.deleteUser', array($userLogin));
}
+ /**
+ * @param string $userLogin
+ */
public function deleteUserAccess($userLogin, $idSites = null)
{
$db = $this->getDb();
if (is_null($idSites)) {
- $db->query("DELETE FROM " . Common::prefixTable("access") .
- " WHERE login = ?",
- array($userLogin));
+ $db->query("DELETE FROM " . Common::prefixTable("access") . " WHERE login = ?", $userLogin);
} else {
foreach ($idSites as $idsite) {
- $db->query("DELETE FROM " . Common::prefixTable("access") .
- " WHERE idsite = ? AND login = ?",
- array($idsite, $userLogin)
- );
+ $db->query("DELETE FROM " . Common::prefixTable("access") . " WHERE idsite = ? AND login = ?", [$idsite, $userLogin]);
}
}
}
@@ -349,4 +403,93 @@ class Model
return Db::get();
}
+ public function getUserLoginsMatching($idSite = null, $pattern = null, $access = null, $logins = null)
+ {
+ $filter = new UserTableFilter($access, $idSite, $pattern, $logins);
+
+ list($joins, $bind) = $filter->getJoins('u');
+ list($where, $whereBind) = $filter->getWhere();
+
+ $bind = array_merge($bind, $whereBind);
+
+ $sql = 'SELECT u.login FROM ' . $this->table . " u $joins $where";
+
+ $db = $this->getDb();
+
+ $result = $db->fetchAll($sql, $bind);
+ $result = array_column($result, 'login');
+ return $result;
+ }
+
+ /**
+ * Returns all users and their access to `$idSite`.
+ *
+ * @param int $idSite
+ * @param int|null $limit
+ * @param int|null $offset
+ * @param string|null $pattern text to search for if any
+ * @param string|null $access 'noaccess','some','view','admin' or 'superuser'
+ * @param string[]|null $logins the logins to limit the search to (if any)
+ * @return array
+ */
+ public function getUsersWithRole($idSite, $limit = null, $offset = null, $pattern = null, $access = null, $logins = null)
+ {
+ $filter = new UserTableFilter($access, $idSite, $pattern, $logins);
+
+ list($joins, $bind) = $filter->getJoins('u');
+ list($where, $whereBind) = $filter->getWhere();
+
+ $bind = array_merge($bind, $whereBind);
+
+ $limitSql = '';
+ if ($limit) {
+ $limitSql = "LIMIT " . (int)$limit;
+ }
+
+ $offsetSql = '';
+ if ($offset) {
+ $offsetSql = "OFFSET " . (int)$offset;
+ }
+
+ $sql = 'SELECT SQL_CALC_FOUND_ROWS u.*, GROUP_CONCAT(a.access SEPARATOR "|") as access
+ FROM ' . $this->table . " u
+ $joins
+ $where
+ GROUP BY u.login
+ ORDER BY u.login ASC
+ $limitSql $offsetSql";
+
+ $db = $this->getDb();
+
+ $users = $db->fetchAll($sql, $bind);
+ foreach ($users as &$user) {
+ $user['access'] = explode('|', $user['access']);
+ }
+
+ $count = $db->fetchOne("SELECT FOUND_ROWS()");
+
+ return [$users, $count];
+ }
+
+ public function getSiteAccessCount($userLogin)
+ {
+ $sql = "SELECT COUNT(*) FROM " . Common::prefixTable('access') . " WHERE login = ?";
+ $bind = [$userLogin];
+
+ $db = $this->getDb();
+ return $db->fetchOne($sql, $bind);
+ }
+
+ public function getUsersWithAccessToSites($idSites)
+ {
+ $idSites = array_map('intval', $idSites);
+
+ $loginSql = 'SELECT DISTINCT ia.login FROM ' . Common::prefixTable('access') . ' ia WHERE ia.idsite IN ('
+ . implode(',', $idSites) . ')';
+
+ $logins = \Piwik\Db::fetchAll($loginSql);
+ $logins = array_column($logins, 'login');
+ return $logins;
+ }
+
}
diff --git a/plugins/UsersManager/Sql/SiteAccessFilter.php b/plugins/UsersManager/Sql/SiteAccessFilter.php
new file mode 100644
index 0000000000..ac8da88164
--- /dev/null
+++ b/plugins/UsersManager/Sql/SiteAccessFilter.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\UsersManager\Sql;
+
+
+use Piwik\Common;
+
+class SiteAccessFilter
+{
+ /**
+ * @var string
+ */
+ private $filterByRole;
+
+ /**
+ * @var int
+ */
+ private $userLogin;
+
+ /**
+ * @var string
+ */
+ private $filterSearch;
+
+ /**
+ * List of sites to limit the search to.
+ *
+ * @var int[]|null
+ */
+ private $idSites;
+
+ public function __construct($userLogin, $filterSearch, $filterByRole, $idSites)
+ {
+ if (empty($userLogin)) {
+ throw new \InvalidArgumentException("filtering by role is only supported for a single site");
+ }
+
+ $this->userLogin = $userLogin;
+ $this->filterSearch = $filterSearch;
+ $this->filterByRole = $filterByRole;
+ $this->idSites = empty($idSites) ? null : array_map('intval', $idSites);
+ }
+
+ public function getJoins($accessTable)
+ {
+ $result = "RIGHT JOIN ". Common::prefixTable('site') . " s ON s.idsite = $accessTable.idsite AND a.login = ?";
+ $bind = [$this->userLogin];
+
+ return [$result, $bind];
+ }
+
+ public function getWhere()
+ {
+ $bind = [];
+ $result = [];
+
+ if ($this->filterSearch) {
+ $bind = array_merge($bind, \Piwik\Plugins\SitesManager\Model::getPatternMatchSqlBind($this->filterSearch));
+ $result[] = \Piwik\Plugins\SitesManager\Model::getPatternMatchSqlQuery('s');
+ }
+
+ if ($this->filterByRole) {
+ if ($this->filterByRole == 'noaccess') {
+ $result[] = 'a.access IS NULL';
+ } else if ($this->filterByRole == 'some') {
+ $result[] = 'a.access IS NOT NULL';
+ } else {
+ $result[] = 'a.access = ?';
+ $bind[] = $this->filterByRole;
+ }
+ }
+
+ if (!empty($this->idSites)) {
+ $result[] = 'a.idsite IN (' . implode(',', $this->idSites) . ')';
+ }
+
+ if (!empty($result)) {
+ $result = 'WHERE ' . implode(' AND ', $result);
+ } else {
+ $result = '';
+ }
+
+ return [$result, $bind];
+ }
+} \ No newline at end of file
diff --git a/plugins/UsersManager/Sql/UserTableFilter.php b/plugins/UsersManager/Sql/UserTableFilter.php
new file mode 100644
index 0000000000..24bd60e55f
--- /dev/null
+++ b/plugins/UsersManager/Sql/UserTableFilter.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\UsersManager\Sql;
+
+
+use Piwik\Access;
+use Piwik\Common;
+use Piwik\Piwik;
+
+class UserTableFilter
+{
+ /**
+ * @var string
+ */
+ private $filterByRole;
+
+ /**
+ * @var int
+ */
+ private $filterByRoleSite;
+
+ /**
+ * @var string
+ */
+ private $filterSearch;
+
+ /**
+ * @var string[]
+ */
+ private $logins;
+
+ public function __construct($filterByRole, $filterByRoleSite, $filterSearch, $logins = null)
+ {
+ $this->filterByRole = $filterByRole;
+ $this->filterByRoleSite = $filterByRoleSite;
+ $this->filterSearch = $filterSearch;
+ $this->logins = $logins;
+
+ if (isset($this->filterByRole) && !isset($this->filterByRoleSite)) {
+ throw new \InvalidArgumentException("filtering by role is only supported for a single site");
+ }
+
+ // can only filter by superuser if current user is a superuser
+ if ($this->filterByRole == 'superuser'
+ && !Piwik::hasUserSuperUserAccess()
+ ) {
+ $this->filterByRole = null;
+ }
+ }
+
+ public function getJoins($userTable)
+ {
+ $result = "LEFT JOIN " . Common::prefixTable('access') . " a ON $userTable.login = a.login AND (a.idsite IS NULL OR a.idsite = ?)";
+ $bind = [$this->filterByRoleSite];
+
+ return [$result, $bind];
+ }
+
+ public function getWhere()
+ {
+ $conditions = [];
+ $bind = [];
+
+ if ($this->filterByRole) {
+ list($filterByRoleSql, $filterByRoleBind) = $this->getAccessSelectSqlCondition();
+
+ $conditions[] = $filterByRoleSql;
+ $bind = array_merge($bind, $filterByRoleBind);
+ }
+
+ if ($this->filterSearch) {
+ $conditions[] = '(u.login LIKE ? OR u.email LIKE ?)';
+ $bind = array_merge($bind, ['%' . $this->filterSearch . '%', '%' . $this->filterSearch . '%']);
+ }
+
+ if ($this->logins !== null) {
+ $logins = array_map('json_encode', $this->logins);
+ $conditions[] = 'u.login IN (' . implode(',', $logins) . ')';
+ }
+
+ $result = implode(' AND ', $conditions);
+ if (!empty($result)) {
+ $result = 'WHERE ' . $result;
+ }
+
+ return [$result, $bind];
+ }
+
+ private function getAccessSelectSqlCondition()
+ {
+ $sql = '';
+ $bind = [];
+
+ switch ($this->filterByRole) {
+ case 'noaccess':
+ $sql = "(a.access IS NULL AND u.superuser_access <> 1)";
+ break;
+ case 'some':
+ $sql = "(a.access IS NOT NULL OR u.superuser_access = 1)";
+ break;
+ case 'view':
+ case 'admin':
+ $sql = "a.access = ?";
+ $bind[] = $this->filterByRole;
+ break;
+ case 'superuser':
+ $sql = "u.superuser_access = 1";
+ break;
+ }
+
+ return [$sql, $bind];
+ }
+} \ No newline at end of file
diff --git a/plugins/UsersManager/UsersManager.php b/plugins/UsersManager/UsersManager.php
index 4f699aab5f..1aa72afbf4 100644
--- a/plugins/UsersManager/UsersManager.php
+++ b/plugins/UsersManager/UsersManager.php
@@ -123,12 +123,12 @@ class UsersManager extends \Piwik\Plugin
*/
public function getJsFiles(&$jsFiles)
{
+ $jsFiles[] = "plugins/UsersManager/angularjs/users-manager/users-manager.component.js";
+ $jsFiles[] = "plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.js";
+ $jsFiles[] = "plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.js";
+ $jsFiles[] = "plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.js";
$jsFiles[] = "plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js";
$jsFiles[] = "plugins/UsersManager/angularjs/personal-settings/anonymous-settings.controller.js";
- $jsFiles[] = "plugins/UsersManager/angularjs/manage-super-user/manage-super-user.controller.js";
- $jsFiles[] = "plugins/UsersManager/angularjs/manage-user-access/manage-user-access.controller.js";
- $jsFiles[] = "plugins/UsersManager/angularjs/manage-users/manage-users.controller.js";
- $jsFiles[] = "plugins/UsersManager/angularjs/give-user-view-access/give-user-view-access.controller.js";
}
/**
@@ -137,6 +137,11 @@ class UsersManager extends \Piwik\Plugin
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/UsersManager/stylesheets/usersManager.less";
+
+ $stylesheets[] = "plugins/UsersManager/angularjs/users-manager/users-manager.component.less";
+ $stylesheets[] = "plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.less";
+ $stylesheets[] = "plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.less";
+ $stylesheets[] = "plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.less";
}
/**
@@ -179,8 +184,7 @@ class UsersManager extends \Piwik\Plugin
Piwik::postEvent('UsersManager.checkPassword', array($password));
if (!self::isValidPasswordString($password)) {
- throw new Exception(Piwik::translate('UsersManager_ExceptionInvalidPassword', array(self::PASSWORD_MIN_LENGTH
- )));
+ throw new Exception(Piwik::translate('UsersManager_ExceptionInvalidPassword', array(self::PASSWORD_MIN_LENGTH)));
}
}
@@ -223,5 +227,58 @@ class UsersManager extends \Piwik\Plugin
$translationKeys[] = "UsersManager_UserHasPermission";
$translationKeys[] = "UsersManager_UserHasNoPermission";
$translationKeys[] = "UsersManager_PrivNone";
+ $translationKeys[] = "UsersManager_ManageUsers";
+ $translationKeys[] = "UsersManager_ManageUsersDesc";
+ $translationKeys[] = 'Mobile_NavigationBack';
+ $translationKeys[] = 'UsersManager_AddExistingUser';
+ $translationKeys[] = 'UsersManager_AddUser';
+ $translationKeys[] = 'UsersManager_EnterUsernameOrEmail';
+ $translationKeys[] = 'UsersManager_NoAccessWarning';
+ $translationKeys[] = 'UsersManager_BulkActions';
+ $translationKeys[] = 'UsersManager_SetPermission';
+ $translationKeys[] = 'UsersManager_RolesHelp';
+ $translationKeys[] = 'UsersManager_Role';
+ $translationKeys[] = 'General_Actions';
+ $translationKeys[] = 'UsersManager_TheDisplayedWebsitesAreSelected';
+ $translationKeys[] = 'UsersManager_ClickToSelectAll';
+ $translationKeys[] = 'UsersManager_AllWebsitesAreSelected';
+ $translationKeys[] = 'UsersManager_ClickToSelectDisplayedWebsites';
+ $translationKeys[] = 'UsersManager_DeletePermConfirmSingle';
+ $translationKeys[] = 'UsersManager_DeletePermConfirmMultiple';
+ $translationKeys[] = 'UsersManager_ChangePermToSiteConfirmSingle';
+ $translationKeys[] = 'UsersManager_ChangePermToSiteConfirmMultiple';
+ $translationKeys[] = 'UsersManager_BasicInformation';
+ $translationKeys[] = 'UsersManager_Permissions';
+ $translationKeys[] = 'UsersManager_RemovePermissions';
+ $translationKeys[] = 'UsersManager_FirstSiteInlineHelp';
+ $translationKeys[] = 'UsersManager_SuperUsersPermissionsNotice';
+ $translationKeys[] = 'UsersManager_SuperUserIntro1';
+ $translationKeys[] = 'UsersManager_SuperUserIntro2';
+ $translationKeys[] = 'UsersManager_HasSuperUserAccess';
+ $translationKeys[] = 'UsersManager_AreYouSure';
+ $translationKeys[] = 'UsersManager_RemoveSuperuserAccessConfirm';
+ $translationKeys[] = 'UsersManager_AddSuperuserAccessConfirm';
+ $translationKeys[] = 'UsersManager_UserSearch';
+ $translationKeys[] = 'UsersManager_DeleteUsers';
+ $translationKeys[] = 'UsersManager_FilterByAccess';
+ $translationKeys[] = 'UsersManager_XtoYofN';
+ $translationKeys[] = 'UsersManager_Username';
+ $translationKeys[] = 'UsersManager_RoleFor';
+ $translationKeys[] = 'UsersManager_TheDisplayedUsersAreSelected';
+ $translationKeys[] = 'UsersManager_AllUsersAreSelected';
+ $translationKeys[] = 'UsersManager_ClickToSelectDisplayedUsers';
+ $translationKeys[] = 'UsersManager_DeleteUserConfirmSingle';
+ $translationKeys[] = 'UsersManager_DeleteUserConfirmMultiple';
+ $translationKeys[] = 'UsersManager_DeleteUserPermConfirmSingle';
+ $translationKeys[] = 'UsersManager_DeleteUserPermConfirmMultiple';
+ $translationKeys[] = 'UsersManager_AddNewUser';
+ $translationKeys[] = 'UsersManager_EditUser';
+ $translationKeys[] = 'UsersManager_CreateUser';
+ $translationKeys[] = 'UsersManager_SaveBasicInfo';
+ $translationKeys[] = 'UsersManager_Email';
+ $translationKeys[] = 'UsersManager_LastSeen';
+ $translationKeys[] = 'UsersManager_SuperUserAccess';
+ $translationKeys[] = 'General_Warning';
+ $translationKeys[] = 'General_Add';
}
}
diff --git a/plugins/UsersManager/angularjs/give-user-view-access/give-user-view-access.controller.js b/plugins/UsersManager/angularjs/give-user-view-access/give-user-view-access.controller.js
deleted file mode 100644
index fa2a01dcea..0000000000
--- a/plugins/UsersManager/angularjs/give-user-view-access/give-user-view-access.controller.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/*!
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-(function () {
- angular.module('piwikApp').controller('GiveUserViewAccessController', GiveUserViewAccessController);
-
- GiveUserViewAccessController.$inject = ['piwikApi', '$window'];
-
- function GiveUserViewAccessController(piwikApi, $window) {
-
- var self = this;
- this.isLoading = false;
- this.showForm = false;
- this.usernameOrEmail = '';
-
- var requestOptions = {placeat: '#ajaxErrorGiveViewAccess'};
-
- function hideLoading() {
- self.isLoading = false;
- }
-
- function showLoading() {
- self.isLoading = true;
- }
-
- function showErrorNotification(errorMessage)
- {
- var placeAt = requestOptions.placeat;
- $(placeAt).show();
-
- var UI = require('piwik/UI');
- var notification = new UI.Notification();
- notification.show(errorMessage, {
- placeat: placeAt,
- context: 'error',
- id: 'ajaxHelper',
- type: null
- });
- notification.scrollToNotification();
- hideLoading();
- }
-
- function sendViewAccess(userLogin)
- {
- var parameters = {userLogin: userLogin, access: 'view', idSites: getIdSites()};
-
- piwikApi.post({
- module: 'API',
- format: 'json',
- method: 'UsersManager.setUserAccess'
- }, parameters, requestOptions).then(function () {
- $window.location.reload();
- hideLoading();
- }, function () {
- hideLoading();
- });
- }
- function getIdSites() {
- return $('#usersManagerSiteSelect').attr('siteid');
- }
-
- function setViewAccessForUserToAllWebsitesIfUserConfirms(userLogin)
- {
- // ask confirmation
- $('#confirm').find('.login').text(userLogin);
-
- function onValidate() {
- sendViewAccess(userLogin);
- }
-
- piwikHelper.modalConfirm('#confirm', {yes: onValidate, no: hideLoading})
- }
-
- function setViewAccessForUserIfNotAlreadyHasAccess(userLogin, idSites)
- {
- piwikApi.fetch({
- method: 'UsersManager.getUsersAccessFromSite',
- userLogin: userLogin,
- idSite: idSites,
- filter_limit: '-1'
- }, requestOptions).then(function (users) {
- var userLogins = [];
- if (users) {
- angular.forEach(users, function (val, key) {
- userLogins.push((''+ key).toLowerCase());
- });
- }
-
- if (-1 !== userLogins.indexOf(userLogin.toLowerCase())) {
- showErrorNotification(_pk_translate('UsersManager_ExceptionUserHasViewAccessAlready'));
- } else {
- sendViewAccess(userLogin);
- }
-
- }, function () {
- hideLoading();
- });
- }
-
- function ifUserExists(usernameOrEmail)
- {
- return piwikApi.fetch({
- method: 'UsersManager.userExists',
- userLogin: usernameOrEmail
- }, requestOptions).then(function (response) {
-
- return response;
-
- }, function () {
- hideLoading();
- });
- }
-
- function getUsernameFromEmail(usernameOrEmail, callback)
- {
- return piwikApi.fetch({
- method: 'UsersManager.getUserLoginFromUserEmail',
- userEmail: usernameOrEmail
- }, requestOptions).then(function (response) {
- return response;
- }, function () {
- hideLoading();
- });
- }
-
- function giveViewAccessToUser(userLogin)
- {
- var idSites = getIdSites();
-
- if (idSites === 'all') {
- setViewAccessForUserToAllWebsitesIfUserConfirms(userLogin);
- } else {
- function onValidate() {
- setViewAccessForUserIfNotAlreadyHasAccess(userLogin, idSites);
- }
-
- if (userLogin == 'anonymous') {
- piwikHelper.modalConfirm('#confirmAnonymousAccess', {yes: onValidate, no: hideLoading})
- } else {
- onValidate();
- }
- }
- }
-
- this.giveAccess = function () {
-
- if (!this.usernameOrEmail) {
- showErrorNotification(_pk_translate('UsersManager_ExceptionNoValueForUsernameOrEmail'));
- return;
- }
-
- showLoading();
-
- ifUserExists(this.usernameOrEmail).then(function (isUserName) {
- if (isUserName && isUserName.value) {
- giveViewAccessToUser(self.usernameOrEmail);
- } else {
- getUsernameFromEmail(self.usernameOrEmail).then(function (login) {
- if (login && login.value) {
- giveViewAccessToUser(login.value);
- } else {
- hideLoading();
- }
- });
- }
- });
- };
-
- this.showViewAccessForm = function () {
- this.showForm = true;
- }
- }
-})(); \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/manage-super-user/manage-super-user.controller.js b/plugins/UsersManager/angularjs/manage-super-user/manage-super-user.controller.js
deleted file mode 100644
index eb8c3ed9d4..0000000000
--- a/plugins/UsersManager/angularjs/manage-super-user/manage-super-user.controller.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*!
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-(function () {
- angular.module('piwikApp').controller('ManageSuperUserController', ManageSuperUserController);
-
- ManageSuperUserController.$inject = ['piwikApi', '$timeout'];
-
- function ManageSuperUserController(piwikApi, $timeout) {
-
- var self = this;
- this.isLoading = false;
-
- function updateSuperUserAccess(login, hasSuperUserAccess)
- {
- self.isLoading = true;
-
- $timeout(function () {
- piwik.helper.lazyScrollTo('.loadingManageSuperUser', 40);
- });
-
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.setSuperUserAccess'
- }, {userLogin: login, hasSuperUserAccess: hasSuperUserAccess}).then(function () {
-
- self.isLoading = false;
-
- var UI = require('piwik/UI');
- var notification = new UI.Notification();
- notification.show(_pk_translate('General_Done'), {
- placeat: '#superUserAccessUpdated',
- context: 'success',
- noclear: true,
- type: 'toast',
- style: {display: 'inline-block', marginTop: '10px', marginBottom: '30px'},
- id: 'usersManagerSuperUserAccessUpdated'
- });
- notification.scrollToNotification();
- piwikHelper.redirect();
-
- }, function () {
- self.isLoading = false;
- });
- }
-
- this.removeSuperUserAccess = function (login) {
- var message = 'UsersManager_ConfirmProhibitOtherUsersSuperUserAccess';
- if (login == piwik.userLogin) {
- message = 'UsersManager_ConfirmProhibitMySuperUserAccess';
- }
-
- message = _pk_translate(message, [login]);
-
- $('#superUserAccessConfirm h2').text(message);
-
- piwikHelper.modalConfirm('#superUserAccessConfirm', {yes: function () {
- updateSuperUserAccess(login, 0);
- }});
- };
-
- this.giveSuperUserAccess = function (login) {
-
- var message = _pk_translate('UsersManager_ConfirmGrantSuperUserAccess', [login]);
-
- $('#superUserAccessConfirm h2').text(message);
-
- piwikHelper.modalConfirm('#superUserAccessConfirm', {yes: function () {
- updateSuperUserAccess(login, 1);
- }});
- };
- }
-})(); \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/manage-user-access/manage-user-access.controller.js b/plugins/UsersManager/angularjs/manage-user-access/manage-user-access.controller.js
deleted file mode 100644
index 96b34ae726..0000000000
--- a/plugins/UsersManager/angularjs/manage-user-access/manage-user-access.controller.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/*!
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-(function () {
- angular.module('piwikApp').controller('ManageUserAccessController', ManageUserAccessController);
-
- ManageUserAccessController.$inject = ['piwik', 'piwikApi', '$timeout'];
-
- function ManageUserAccessController(piwik, piwikApi, $timeout) {
-
- var self = this;
- this.isLoading = false;
-
- function launchAjaxRequest(login, access, successCallback) {
-
- self.isLoading = true;
-
- $timeout(function () {
- piwik.helper.lazyScrollTo('.loadingManageUserAccess', 50);
- });
-
- var parameters = {userLogin: login, access: access, idSites: self.site.id};
-
- return piwikApi.post({
- module: 'API',
- format: 'json',
- method: 'UsersManager.setUserAccess'
- }, parameters).then(function (response) {
- self.isLoading = false;
- return response;
- }, function () {
- self.isLoading = false;
- });
- }
-
- this.siteChanged = function () {
- if (this.site && this.site.id != piwik.idSite) {
- piwik.broadcast.propagateNewPage('segment=&idSite=' + this.site.id, false);
- }
- };
-
- this.setAccess = function (login, access) {
- login = String(login);
- login=piwik.helper.escape(piwik.helper.htmlEntities(login));
- if ( $('[data-login="' + login + '"]').find("#"+access).has('.accessGranted').length ){
- return;
- }
- // callback called when the ajax request Update the user permissions is successful
- function successCallback(response) {
- var mainDiv = $('[data-login="' + login + '"]');
- var grantedDiv = mainDiv.find('.accessGranted');
- var currentSite = $(".sites_autocomplete").attr("sitename");
- currentSite = piwik.helper.escape(piwik.helper.htmlEntities(currentSite));
-
- grantedDiv.attr("src", "plugins/UsersManager/images/no-access.png")
- .attr("class", "updateAccess")
- .attr("title", function(){
- var access = grantedDiv.parents('[id]').attr('id');
- if (access =="noaccess"){
- return _pk_translate('UsersManager_RemoveUserAccess', [login,currentSite])
- }
- else if (access =="view") {
- return _pk_translate('UsersManager_GiveUserAccess', [login,_pk_translate('UsersManager_PrivView'),currentSite]);
- }
- else if (access =="admin") {
- return _pk_translate('UsersManager_GiveUserAccess', [login,_pk_translate('UsersManager_PrivAdmin'),currentSite]);
- }
- })
- .off('click')
- .click(function () {
- var access = $(this).parent().attr('id')
- self.setAccess(login, access);
- })
- ;
- mainDiv.find('#' + access + ' img')
- .attr('src', "plugins/UsersManager/images/ok.png")
- .attr('class', "accessGranted")
- .attr("title",function(){
- if(access=="noaccess"){
- return _pk_translate('UsersManager_UserHasNoPermission', [login,_pk_translate('UsersManager_PrivNone'),currentSite]);
- }else {
- return _pk_translate('UsersManager_UserHasPermission', [login,access,currentSite]);
- }}
- )
- ;
-
- var UI = require('piwik/UI');
- var notification = new UI.Notification();
- notification.show(_pk_translate('General_Done'), {
- placeat: '#accessUpdated',
- context: 'success',
- noclear: true,
- type: 'toast',
- style: {display: 'inline-block', marginTop: '10px'},
- id: 'usersManagerAccessUpdated'
- });
-
- // reload if user anonymous was updated, since we display a Notice message when anon has view access
- if (login == 'anonymous') {
- window.location.reload();
- }
- }
-
- function onValidate() {
- launchAjaxRequest(login, access).then(successCallback);
- }
-
- if (login == 'anonymous' && access == 'view') {
- piwikHelper.modalConfirm('#confirmAnonymousAccess', {yes: onValidate})
- }
- else if (this.site.id == 'all') {
-
- //ask confirmation
- $('#confirm').find('.login').text(login);
-
- piwikHelper.modalConfirm('#confirm', {yes: onValidate})
- }
- else {
- onValidate();
- }
- }
- }
-})();
diff --git a/plugins/UsersManager/angularjs/manage-users/manage-users.controller.js b/plugins/UsersManager/angularjs/manage-users/manage-users.controller.js
deleted file mode 100644
index 1ca2ae0c88..0000000000
--- a/plugins/UsersManager/angularjs/manage-users/manage-users.controller.js
+++ /dev/null
@@ -1,217 +0,0 @@
-/*!
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-(function () {
- angular.module('piwikApp').controller('ManageUsersController', ManageUsersController);
-
- ManageUsersController.$inject = ['piwik', 'piwikApi', '$timeout', '$rootScope'];
-
- function ManageUsersController(piwik, piwikApi, $timeout, $rootScope) {
- // remember to keep controller very simple. Create a service/factory (model) if needed
-
- var self = this;
- var alreadyEdited = {};
-
- this.isLoading = false;
- this.showCreateUser = true;
-
- function setIsLoading()
- {
- self.isLoading = true;
- $timeout(function () {
- piwik.helper.lazyScrollTo('.loadingManageUsers', 50);
- });
- }
-
- function sendUpdateUserAJAX(row) {
- var parameters = {};
- parameters.userLogin = $(row).children('#userLogin').html();
- var password = $(row).find('input#password').val();
- if (password != '-') parameters.password = password;
- parameters.email = $(row).find('input#email').val();
- parameters.alias = $(row).find('input#alias').val();
-
- setIsLoading();
-
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.updateUser'
- }, parameters).then(function () {
- piwik.helper.redirect();
- self.isLoading = false;
- }, function () {
- self.isLoading = false;
- });
- }
-
- function sendDeleteUserAJAX(login) {
-
- setIsLoading();
-
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.deleteUser'
- }, {userLogin: login}).then(function () {
- piwik.helper.redirect();
- self.isLoading = false;
- }, function () {
- self.isLoading = false;
- });
- }
-
- function sendAddUserAJAX(row) {
- var parameters = {};
- parameters.userLogin = $(row).find('input#useradd_login').val();
- parameters.password = $(row).find('input#useradd_password').val();
- parameters.email = $(row).find('input#useradd_email').val();
- parameters.alias = $(row).find('input#useradd_alias').val();
-
- setIsLoading();
-
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.addUser'
- }, parameters).then(function () {
- piwik.helper.redirect();
- self.isLoading = false;
- }, function () {
- self.isLoading = false;
- });
- }
-
- function submitOnEnter(e) {
- var key = e.keyCode || e.which;
- if (key == 13) {
- $(this).find('.adduser').click();
- $(this).find('.updateuser').click();
- }
- }
-
- this.editUser = function (idRow) {
- if (alreadyEdited[idRow] == 1) {
- return;
- }
-
- alreadyEdited[idRow] = 1;
-
- var $row = $('tr#' + idRow);
-
- $row.find('.editable').keypress(submitOnEnter);
- $row.find('.editable').each(
- // make the fields editable
- // change the EDIT button to VALID button
- function (i, n) {
- var idName = $(n).attr('id');
- var contentBefore = idName != 'password' ? $(n).text() : '';
- var contentAfter = '<input id="' + idName + '" value="' + piwikHelper.htmlEntities(contentBefore) + '" size="25" />';
- $(n).html(contentAfter);
- }
- );
-
- var $delete = $row.find('.edituser');
-
- $delete
- .toggle()
- .parent()
- .prepend($('<a class="canceluser">' + _pk_translate('General_OrCancel', ['', '']) + '</a>')
- .click(function () {
- piwikHelper.redirect();
- })
- ).prepend($('<input type="submit" class="btn updateuser" value="' + _pk_translate('General_Save') + '" />')
- .click(function () {
- var $tr = $('tr#' + idRow);
-
- sendUpdateUserAJAX($tr);
- })
- );
- }
-
- this.createUser = function () {
-
- var parameters = {isAllowed: true};
- $rootScope.$emit('UsersManager.initAddUser', parameters);
- if (parameters && !parameters.isAllowed) {
- return;
- }
-
- this.showCreateUser = false;
-
- var numberOfRows = $('table#users')[0].rows.length;
- var newRowId = numberOfRows + 1;
- newRowId = 'row' + newRowId;
-
- $($.parseHTML(' <tr id="' + newRowId + '" class="addNewUserRow">\
- <td><input id="useradd_login" placeholder="username" size="10" maxlength="100" /></td>\
- <td><input id="useradd_password" placeholder="password" size="10" /></td>\
- <td><input id="useradd_email" placeholder="email@domain.com" size="15" maxlength="100" /></td>\
- <td><input id="useradd_alias" placeholder="alias" size="15" maxlength="45" /></td>\
- <td>-</td>\
- <td>-</td>\
- <td><input type="submit" class="btn adduser" value="' + _pk_translate('General_Save') + '" /></td>\
- <td><span class="cancel">' + sprintf(_pk_translate('General_OrCancel'), "", "") + '</span></td>\
- </tr>')
- ).appendTo('#users');
- $('#' + newRowId).keypress(submitOnEnter);
- $('.adduser').click(function () { sendAddUserAJAX($('tr#' + newRowId)); });
- $('.cancel').click(function () {
- piwikHelper.hideAjaxError();
- $(this).parents('tr').remove();
- $('.add-user').toggle();
- self.showCreateUser = true;
- });
- };
-
- this.deleteUser = function (loginToDelete) {
-
- var idRow = $(this).attr('id');
-
- var message = _pk_translate('UsersManager_DeleteConfirm');
- $('#confirmUserRemove').find('h2').text(sprintf(message, '"' + loginToDelete + '"'));
-
- piwikHelper.modalConfirm('#confirmUserRemove', {yes: function () {
- sendDeleteUserAJAX(loginToDelete);
- }});
- };
-
- this.regenerateUserTokenAuth = function (userLogin) {
- var parameters = { userLogin: userLogin };
- var confirm = '#confirmTokenRegenerate';
-
- if (userLogin == piwik.userLogin) {
- confirm = '#confirmTokenRegenerateSelf';
- }
-
- piwikHelper.modalConfirm(confirm, {yes: function () {
- setIsLoading();
-
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.regenerateTokenAuth'
- }, parameters).then(function () {
- piwik.helper.redirect();
- self.isLoading = false;
- }, function () {
- self.isLoading = false;
- });
- }});
- };
-
- $(document).ready(function () {
- var alreadyEdited = [];
- // when click on edituser, the cells become editable
-
- // Show the token_auth
- $('.token_auth').click(function () {
- var token = $(this).data('token');
-
- if ($('.token_auth_content', this).text() != token) {
- $('.token_auth_content', this).text(token);
- }
- });
- });
-
- }
-})(); \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.html b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.html
new file mode 100644
index 0000000000..1b75f3ebc8
--- /dev/null
+++ b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.html
@@ -0,0 +1,201 @@
+<div class="pagedUsersList" ng-class="{loading: $ctrl.isLoadingUsers}">
+ <div class="userListFilters row">
+ <div class="col s12 m12 l6">
+ <div class="input-field col s12 m4 l4">
+ <a
+ class='dropdown-trigger btn bulk-actions'
+ href=''
+ data-activates='user-list-bulk-actions'
+ piwik-dropdown-menu
+ ng-class="{ disabled: $ctrl.isBulkActionsDisabled }"
+ >
+ {{:: 'UsersManager_BulkActions'|translate }}
+ </a>
+ <ul id='user-list-bulk-actions' class='dropdown-content'>
+ <li>
+ <a
+ class='dropdown-trigger'
+ data-activates="bulk-set-access"
+ piwik-dropdown-menu
+ >
+ {{:: 'UsersManager_SetPermission'|translate }}
+ </a>
+ <ul id="bulk-set-access" class="dropdown-content">
+ <li ng-repeat="access in $ctrl.bulkActionAccessLevels">
+ <a
+ href=""
+ ng-click="$ctrl.userToChange = null; $ctrl.roleToChangeTo = access.key; $ctrl.showAccessChangeConfirm();"
+ >
+ {{ access.value }}
+ </a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href=""
+ ng-click="$ctrl.userToChange = null; $ctrl.roleToChangeTo = 'noaccess'; $ctrl.showAccessChangeConfirm();"
+ >
+ {{:: 'UsersManager_RemovePermissions'|translate }}
+ </a>
+ </li>
+ <li ng-if="$ctrl.currentUserRole == 'superuser'">
+ <a href="" ng-click="$ctrl.showDeleteConfirm()">{{:: 'UsersManager_DeleteUsers'|translate }}</a>
+ </li>
+ </ul>
+ </div>
+ <div class="input-field col s12 m4 l4">
+ <div
+ piwik-field
+ name="user-text-filter"
+ class="permissions-for-selector"
+ uicontrol="text"
+ ng-model="$ctrl.userTextFilter"
+ ng-model-options="{debounce: 300}"
+ placeholder="{{:: 'UsersManager_UserSearch'|translate }}"
+ full-width="true"
+ ng-change="$ctrl.changeSearch({ filter_search: $ctrl.userTextFilter })"
+ ></div>
+ </div>
+ <div class="input-field col s12 m4 l4">
+ <div
+ piwik-field
+ name="access-level-filter"
+ uicontrol="select"
+ ng-model="$ctrl.accessLevelFilter"
+ placeholder="{{:: 'UsersManager_FilterByAccess'|translate }}"
+ options="$ctrl.filterAccessLevels"
+ full-width="true"
+ ng-change="$ctrl.changeSearch({ filter_access: $ctrl.accessLevelFilter })"
+ ></div>
+ </div>
+ </div>
+ <div class="input-field col s12 m12 l6 users-list-pagination-container" ng-if="$ctrl.totalEntries > $ctrl.searchParams.limit">
+ <div class="usersListPagination">
+ <a class="btn prev" ng-class="{ disabled: $ctrl.searchParams.offset <= 0 }" ng-click="$ctrl.gotoPreviousPage()">
+ <span class="pointer">« {{:: 'General_Previous'|translate }}</span>
+ </a>
+
+ <div class="counter">
+ <span>
+ {{ 'UsersManager_XtoYofN'|translate:$ctrl.searchParams.offset:$ctrl.getPaginationUpperBound():$ctrl.totalEntries }}
+ </span>
+ <div piwik-activity-indicator ng-if="$ctrl.isLoadingUsers" loading="$ctrl.isLoadingUsers"></div>
+ </div>
+
+ <a class="btn next" ng-class="{ disabled: $ctrl.searchParams.offset + $ctrl.searchParams.limit >= $ctrl.totalEntries }" ng-click="$ctrl.gotoNextPage()">
+ <span class="pointer">{{:: 'General_Next'|translate }} »</span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div piwik-notification context="info" type="persistent" noclear="true" ng-if="$ctrl.isRoleHelpToggled" class="roles-help-notification">
+ <span piwik-translate="UsersManager_RolesHelp"><a href='https://matomo.org/faq/general/faq_70/' target='_blank' rel='noreferrer noopener'>::</a>::<a href='https://matomo.org/faq/general/faq_69/' target='_blank' rel='noreferrer noopener'>::</a></span>
+ </div>
+
+ <div piwik-content-block>
+ <table piwik-content-table id="manageUsersTable" ng-class="{ loading: $ctrl.isLoadingUsers }">
+ <thead>
+ <tr>
+ <th class="select-cell">
+ <span class="checkbox-container">
+ <input type="checkbox" id="paged_users_select_all" checked="checked" ng-model="$ctrl.isAllCheckboxSelected" ng-change="$ctrl.onAllCheckboxChange()" />
+ <label for="paged_users_select_all"></label>
+ </span>
+ </th>
+ <th class='first'>{{:: 'UsersManager_Username'|translate }}</th>
+ <th class="role_header">
+ <span>{{:: 'UsersManager_RoleFor'|translate }}</span>
+ <a href="" class="helpIcon" ng-click="$ctrl.isRoleHelpToggled = !$ctrl.isRoleHelpToggled" ng-class="{ sticky: $ctrl.isRoleHelpToggled }">
+ <span class="icon-help"></span>
+ </a>
+
+ <div
+ piwik-field
+ class="permissions-for-selector"
+ uicontrol="site"
+ ng-model="$ctrl.permissionsForSite"
+ ng-change="$ctrl.changeSearch({ idSite: $ctrl.permissionsForSite.id })"
+ ui-control-attributes="{ onlySitesWithAdminAccess: $ctrl.currentUserRole !== 'superuser' }"
+ ></div>
+ </th>
+ <th ng-if="$ctrl.currentUserRole == 'superuser'">{{:: 'UsersManager_Email'|translate }}</th>
+ <th ng-if="$ctrl.currentUserRole == 'superuser'">{{:: 'UsersManager_LastSeen'|translate }}</th>
+ <th class="actions-cell-header"><div>{{:: 'General_Actions'|translate }}</div></th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr class="select-all-row" ng-if="$ctrl.isAllCheckboxSelected && $ctrl.users.length && $ctrl.users.length < $ctrl.totalEntries">
+ <td colspan="7">
+ <div ng-if="!$ctrl.areAllResultsSelected">
+ <span piwik-translate="UsersManager_TheDisplayedUsersAreSelected"><strong>{{ $ctrl.users.length }}</strong></span>
+ <a class="toggle-select-all-in-search" href="#" ng-click="$ctrl.areAllResultsSelected = !$ctrl.areAllResultsSelected" piwik-translate="UsersManager_ClickToSelectAll"><strong>{{ $ctrl.totalEntries }}</strong></a>
+ </div>
+
+ <div ng-if="$ctrl.areAllResultsSelected">
+ <span piwik-translate="UsersManager_AllUsersAreSelected"><strong>{{ $ctrl.totalEntries }}</strong></span>
+ <a class="toggle-select-all-in-search" href="#" ng-click="$ctrl.areAllResultsSelected = !$ctrl.areAllResultsSelected" piwik-translate="UsersManager_ClickToSelectDisplayedUsers"><strong>{{ $ctrl.users.length }}</strong></a>
+ </div>
+ </td>
+ </tr>
+
+ <tr ng-repeat="user in $ctrl.users" ng-attr-id="row{{ $index }}">
+ <td class="select-cell">
+ <span class="checkbox-container">
+ <input type="checkbox" ng-attr-id="paged_users_select_row{{ $index }}" checked="checked" ng-model="$ctrl.selectedRows[$index]" ng-click="$ctrl.onRowSelected()" />
+ <label ng-attr-for="paged_users_select_row{{ $index }}"></label>
+ </span>
+ </td>
+ <td id="userLogin">{{ user.login }}</td>
+ <td class="access-cell">
+ <div
+ piwik-field
+ uicontrol="select"
+ ng-model="user.role"
+ options="$ctrl.accessLevels"
+ ng-change="$ctrl.userToChange = user; $ctrl.roleToChangeTo = user.role; $ctrl.previousRole = '{{ user.role }}'; $ctrl.showAccessChangeConfirm();"
+ disabled="user.role == 'superuser'"
+ ></div>
+ </td>
+ <td id="email" ng-if="$ctrl.currentUserRole == 'superuser'">{{ user.email }}</td>
+ <td id="last_seen" ng-if="$ctrl.currentUserRole == 'superuser'">
+ {{ user.last_seen ? (user.last_seen + ' ago') : '-' }}
+ </td>
+ <td class="center actions-cell">
+ <button class="edituser table-action" title="Edit" ng-click="$ctrl.onEditUser({ user: user })">
+ <span class="icon-edit"></span>
+ </button>
+ <button class="deleteuser table-action" title="Delete" ng-click="$ctrl.userToChange = user; $ctrl.showDeleteConfirm()" ng-if="$ctrl.currentUserRole == 'superuser'">
+ <span class="icon-delete"></span>
+ </button>
+ </td>
+ </tr>
+
+ </tbody>
+ </table>
+ </div>
+
+ <div class="delete-user-confirm-modal modal">
+ <div class="modal-content">
+ <h3 ng-if="$ctrl.userToChange" piwik-translate="UsersManager_DeleteUserConfirmSingle"><strong>{{ $ctrl.userToChange.login }}</strong></h3>
+ <p ng-if="!$ctrl.userToChange" piwik-translate="UsersManager_DeleteUserConfirmMultiple"><strong>{{ $ctrl.getAffectedUsersCount() }}</strong></p>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.deleteRequestedUsers()">{{:: 'General_Yes'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.userToChange = null; $ctrl.roleToChangeTo = null;">{{:: 'General_No'|translate }}</a>
+ </div>
+ </div>
+
+ <div class="change-user-role-confirm-modal modal">
+ <div class="modal-content">
+ <h3 ng-if="$ctrl.userToChange" piwik-translate="UsersManager_DeleteUserPermConfirmSingle"><strong>{{ $ctrl.userToChange.login }}</strong>::<strong>{{ $ctrl.getRoleDisplay($ctrl.roleToChangeTo) }}</strong>::<strong ng-bind-html="$ctrl.permissionsForSite.name"></strong></h3>
+ <p ng-if="!$ctrl.userToChange" piwik-translate="UsersManager_DeleteUserPermConfirmMultiple"><strong>{{ $ctrl.getAffectedUsersCount() }}</strong>::<strong>{{ $ctrl.getRoleDisplay($ctrl.roleToChangeTo) }}</strong>::<strong ng-bind-html="$ctrl.permissionsForSite.name"></strong></p>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.changeUserRole()">{{:: 'General_Yes'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.userToChange.role = $ctrl.previousRole; $ctrl.userToChange = null; $ctrl.roleToChangeTo = null;">{{:: 'General_No'|translate }}</a>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.js b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.js
new file mode 100644
index 0000000000..c7b0d4ee49
--- /dev/null
+++ b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.js
@@ -0,0 +1,213 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <piwik-paged-users-list>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikPagedUsersList', {
+ templateUrl: 'plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ onEditUser: '&',
+ onChangeUserRole: '&',
+ onDeleteUser: '&',
+ onSearchChange: '&',
+ initialSiteId: '<',
+ initialSiteName: '<',
+ currentUserRole: '<',
+ isLoadingUsers: '<',
+ accessLevels: '<',
+ filterAccessLevels: '<',
+ totalEntries: '<',
+ users: '<',
+ searchParams: '<'
+ },
+ controller: PagedUsersListController
+ });
+
+ PagedUsersListController.$inject = ['$element'];
+
+ function PagedUsersListController($element) {
+ var vm = this;
+
+ // options for selects
+ vm.bulkActionAccessLevels = null;
+
+ // selection state
+ vm.areAllResultsSelected = false;
+ vm.selectedRows = {};
+ vm.isAllCheckboxSelected = false;
+
+ // intermediate state
+ vm.isBulkActionsDisabled = true;
+ vm.userToChange = null;
+ vm.roleToChangeTo = null;
+ vm.previousRole = null;
+ vm.accessLevelFilter = '';
+
+ // other state
+ vm.isRoleHelpToggled = false;
+
+ vm.$onInit = $onInit;
+ vm.$onChanges = $onChanges;
+ vm.onAllCheckboxChange = onAllCheckboxChange;
+ vm.changeUserRole = changeUserRole;
+ vm.onRowSelected = onRowSelected;
+ vm.deleteRequestedUsers = deleteRequestedUsers;
+ vm.getPaginationUpperBound = getPaginationUpperBound;
+ vm.showDeleteConfirm = showDeleteConfirm;
+ vm.getAffectedUsersCount = getAffectedUsersCount;
+ vm.showAccessChangeConfirm = showAccessChangeConfirm;
+ vm.getRoleDisplay = getRoleDisplay;
+ vm.changeSearch = changeSearch;
+ vm.gotoPreviousPage = gotoPreviousPage;
+ vm.gotoNextPage = gotoNextPage;
+
+ function changeSearch(changes) {
+ var newParams = $.extend({}, vm.searchParams, changes);
+ vm.onSearchChange({ params: newParams });
+ }
+
+ function $onInit() {
+ vm.permissionsForSite = {
+ id: vm.initialSiteId,
+ name: vm.initialSiteName
+ };
+
+ vm.bulkActionAccessLevels = [];
+ vm.accessLevels.forEach(function (entry) {
+ if (entry.key !== 'noaccess' && entry.key !== 'superuser') {
+ vm.bulkActionAccessLevels.push(entry);
+ }
+ });
+ }
+
+ function $onChanges(changes) {
+ if (changes.users) {
+ clearSelection();
+ }
+ }
+
+ function onAllCheckboxChange() {
+ if (!vm.isAllCheckboxSelected) {
+ clearSelection();
+ } else {
+ for (var i = 0; i !== vm.users.length; ++i) {
+ vm.selectedRows[i] = true;
+ }
+ vm.isBulkActionsDisabled = false;
+ }
+ }
+
+ function clearSelection() {
+ vm.selectedRows = {};
+ vm.areAllResultsSelected = false;
+ vm.isBulkActionsDisabled = true;
+ vm.isAllCheckboxSelected = false;
+ vm.userToChange = null;
+ }
+
+ function changeUserRole() {
+ vm.onChangeUserRole({
+ users: getUserOperationSubject(),
+ role: vm.roleToChangeTo
+ });
+ }
+
+ function deleteRequestedUsers() {
+ vm.onDeleteUser({
+ users: getUserOperationSubject(),
+ });
+ }
+
+ function getUserOperationSubject() {
+ if (vm.userToChange) {
+ return [vm.userToChange];
+ } else if (vm.areAllResultsSelected) {
+ return 'all';
+ } else {
+ return getSelectedUsers();
+ }
+ }
+
+ function showAccessChangeConfirm() {
+ $element.find('.change-user-role-confirm-modal').openModal({ dismissible: false });
+ }
+
+ function getAffectedUsersCount() {
+ if (vm.areAllResultsSelected) {
+ return vm.totalEntries;
+ }
+
+ return getSelectedCount();
+ }
+
+ function onRowSelected() {
+ var selectedRowKeyCount = getSelectedCount();
+ vm.isBulkActionsDisabled = selectedRowKeyCount === 0;
+ vm.isAllCheckboxSelected = selectedRowKeyCount === vm.users.length;
+ }
+
+ function getSelectedCount() {
+ var selectedRowKeyCount = 0;
+ Object.keys(vm.selectedRows).forEach(function (key) {
+ if (vm.selectedRows[key]) {
+ ++selectedRowKeyCount;
+ }
+ });
+ return selectedRowKeyCount;
+ }
+
+ function getSelectedUsers() {
+ var result = [];
+ Object.keys(vm.selectedRows).forEach(function (index) {
+ if (vm.selectedRows[index]
+ && vm.users[index] // sanity check
+ ) {
+ result.push(vm.users[index]);
+ }
+ });
+ return result;
+ }
+
+ function getPaginationUpperBound() {
+ return Math.min(vm.searchParams.offset + vm.searchParams.limit, vm.totalEntries);
+ }
+
+ function showDeleteConfirm() {
+ $element.find('.delete-user-confirm-modal').openModal({ dismissible: false });
+ }
+
+ function getRoleDisplay(role) {
+ var result = null;
+ vm.accessLevels.forEach(function (entry) {
+ if (entry.key === role) {
+ result = entry.value;
+ }
+ });
+ return result;
+ }
+
+ function gotoPreviousPage() {
+ changeSearch({
+ offset: Math.max(0, vm.searchParams.offset - vm.searchParams.limit)
+ });
+ }
+
+ function gotoNextPage() {
+ var newOffset = vm.searchParams.offset + vm.searchParams.limit;
+ if (newOffset >= vm.totalEntries) {
+ return;
+ }
+
+ changeSearch({
+ offset: newOffset,
+ });
+ }
+ }
+})();
diff --git a/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.less b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.less
new file mode 100644
index 0000000000..4be91c2266
--- /dev/null
+++ b/plugins/UsersManager/angularjs/paged-users-list/paged-users-list.component.less
@@ -0,0 +1,203 @@
+piwik-paged-users-list {
+ display: block;
+ position: relative;
+
+ [piwik-siteselector] {
+ display: inline-block;
+ margin-left: .3rem;
+ }
+
+ .access-display-control {
+ position: absolute;
+ margin-left: .1rem;
+
+ label {
+ color: @theme-color-text;
+ }
+ }
+
+ .card {
+ margin-top: 0;
+ margin-bottom: 20px;
+ }
+
+ .card .card-content {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ table.entityTable tbody tr td {
+ vertical-align: middle !important;
+ }
+
+ table.entityTable tbody tr td.actions-cell {
+ width: 130px;
+ padding: 0;
+ text-align: center;
+ }
+
+ table.entityTable th.actions-cell-header > div {
+ text-align: center;
+ }
+
+ table#manageUsersTable {
+ [piwik-field] {
+ .form-group {
+ margin: 0;
+ }
+ .input-field {
+ margin-top: 0;
+ padding: 0;
+ }
+ }
+
+ .select-wrapper {
+ transform: scale(.8);
+ width: 100px;
+
+ input {
+ margin-bottom: 0;
+ padding-bottom: .3em;
+ height: 1em;
+ line-height: 1em;
+ }
+
+ span.caret {
+ top: 0;
+ }
+ }
+
+ th.role_header {
+ .helpIcon {
+ color: #9e9e9e;
+ font-size: .8rem;
+ margin-left: .1rem;
+ text-decoration: none;
+
+ &:hover,&.sticky {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ tbody span.checkbox-container {
+ label {
+ transform: scale(.8);
+ height: 1em;
+ line-height: 1em;
+ }
+ }
+
+ .select-cell {
+ width: 32px;
+ }
+
+ table.entityTable tbody tr.select-all-row > td {
+ padding: 6px;
+ text-align: center;
+ }
+
+ .sites_autocomplete {
+ display: block;
+ margin-left: 0;
+ }
+
+ .userListFilters {
+ > .col > .input-field {
+ display: inline-block;
+ vertical-align: top;
+ padding: 0;
+ }
+
+ .form-group, [piwik-form-field] .input-field {
+ margin: 0;
+ }
+ [piwik-form-field] input {
+ margin-bottom: 0;
+ }
+
+ .input-field > .btn {
+ margin-top: .7rem;
+ }
+
+ &.row {
+ margin-bottom: 0;
+ margin-left: -0.75rem;
+ margin-right: -0.75rem;
+ }
+ }
+
+ .usersListPagination {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: .7rem;
+ float: right;
+
+ .pointer {
+ cursor: pointer;
+ }
+
+ div.counter {
+ display: inline-block;
+ line-height: 36px;
+ vertical-align: bottom;
+ flex: 1;
+ text-align: center;
+ margin-left: 10px;
+ margin-right: 10px;
+ }
+ }
+
+ .delete-user-confirm-modal,.change-user-role-confirm-modal {
+ .modal-no {
+ float: right;
+ margin-right: 1em;
+ margin-top: 1em;
+ }
+ }
+
+
+ .pagedUsersList.loading {
+ table {
+ opacity: 0.5;
+ }
+
+ a, input, select, button, label {
+ pointer-events: none;
+ }
+
+ div.counter {
+ position: relative;
+ > span {
+ opacity: 0;
+ }
+ }
+
+ div[piwik-activity-indicator] {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ span {
+ display: none;
+ }
+ }
+ }
+
+ .roles-help-notification {
+ margin-top: 1rem;
+ }
+}
+
+#root piwik-paged-users-list .siteSelector.borderedControl {
+ background-color: white;
+ width: 150px;
+}
+
+#content piwik-paged-users-list .sites_autocomplete > .siteSelector {
+ position: static;
+}
diff --git a/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.html b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.html
new file mode 100644
index 0000000000..048709081c
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.html
@@ -0,0 +1,145 @@
+<div
+ piwik-content-block
+ content-title="{{ $ctrl.getFormTitle() }}"
+ class="userEditForm"
+ ng-class="{ loading: $ctrl.isSavingUserInfo }"
+>
+ <div class="row" piwik-form>
+ <div class="col m2 entityList" ng-if="!$ctrl.isAdd">
+ <ul class="listCircle">
+ <li ng-class="{active: $ctrl.activeTab === 'basic'}" class="menuBasicInfo">
+ <a href="" ng-click="$ctrl.activeTab = 'basic'">{{:: 'UsersManager_BasicInformation'|translate }}</a>
+ </li>
+
+ <li ng-class="{active: $ctrl.activeTab === 'permissions'}" class="menuPermissions">
+ <a href="" ng-click="$ctrl.activeTab = 'permissions'">
+ {{:: 'UsersManager_Permissions'|translate }}
+ </a>
+ <span class="icon-warning" ng-if="!$ctrl.userHasAccess && !$ctrl.user.superuser_access"></span>
+ </li>
+
+ <li ng-class="{active: $ctrl.activeTab === 'superuser'}" class="menuSuperuser" ng-if="$ctrl.currentUserRole == 'superuser'">
+ <a href="" ng-click="$ctrl.activeTab = 'superuser'">{{:: 'UsersManager_SuperUserAccess'|translate }}</a>
+ </li>
+ </ul>
+
+ <div class="save-button-spacer hide-on-small-only">
+ </div>
+
+ <div class='entityCancel' ng-click="$ctrl.onDoneEditing({ isUserModified: $ctrl.isUserModified })">
+ <a href="" class="entityCancelLink">{{:: 'Mobile_NavigationBack'|translate }}</a>
+ </div>
+ </div>
+
+ <div class="visibleTab col m10">
+ <div ng-if="$ctrl.activeTab === 'basic'" class="basic-info-tab">
+ <div
+ piwik-field
+ uicontrol="text"
+ name="user_login"
+ ng-model="$ctrl.user.login"
+ title="Username"
+ maxlength="100"
+ disabled="$ctrl.isSavingUserInfo || !$ctrl.isAdd"
+ >
+ </div>
+
+ <div
+ piwik-field
+ uicontrol="password"
+ name="user_password"
+ ng-model="$ctrl.user.password"
+ title="Password"
+ ng-change="$ctrl.isPasswordChanged = true;"
+ disabled="$ctrl.isSavingUserInfo || ($ctrl.currentUserRole != 'superuser' && !$ctrl.isAdd)"
+ >
+ </div>
+
+ <div
+ piwik-field
+ uicontrol="text"
+ name="user_email"
+ ng-model="$ctrl.user.email"
+ title="Email"
+ maxlength="100"
+ disabled="$ctrl.isSavingUserInfo || ($ctrl.currentUserRole != 'superuser' && !$ctrl.isAdd)"
+ ng-if="$ctrl.currentUserRole == 'superuser' || $ctrl.isAdd"
+ >
+ </div>
+
+ <div
+ piwik-field
+ uicontrol="site"
+ name="user_site"
+ ng-model="$ctrl.firstSiteAccess"
+ title="First website permission"
+ disabled="$ctrl.isSavingUserInfo"
+ ng-if="$ctrl.isAdd"
+ ui-control-attributes="{ onlySitesWithAdminAccess: true }"
+ ng-attr-inline-help="{{:: 'UsersManager_FirstSiteInlineHelp'|translate }}"
+ >
+ </div>
+
+ <div piwik-save-button
+ saving="$ctrl.isSavingUserInfo"
+ onconfirm="$ctrl.saveUserInfo()"
+ ng-value="$ctrl.getSaveButtonLabel()"
+ disabled="$ctrl.isAdd && (!$ctrl.firstSiteAccess || !$ctrl.firstSiteAccess.id)"
+ ng-if="$ctrl.currentUserRole == 'superuser' || $ctrl.isAdd"
+ ></div>
+
+ <div class='entityCancel' ng-click="$ctrl.onDoneEditing({ isUserModified: $ctrl.isUserModified })" ng-if="$ctrl.isAdd">
+ <a href="" class="entityCancelLink">{{:: 'General_Cancel'|translate }}</a>
+ </div>
+ </div>
+
+ <div ng-if="!$ctrl.isAdd" ng-show="$ctrl.activeTab === 'permissions'" class="user-permissions">
+ <piwik-user-permissions-edit
+ user-login="$ctrl.user.login"
+ ng-if="!$ctrl.user.superuser_access"
+ on-user-has-access-detected="$ctrl.userHasAccess = hasAccess"
+ on-access-change="$ctrl.isUserModified = true"
+ access-levels="$ctrl.accessLevels"
+ filter-access-levels="$ctrl.filterAccessLevels"
+ >
+ </piwik-user-permissions-edit>
+ <div ng-if="$ctrl.user.superuser_access" class="alert alert-info">
+ {{:: 'UsersManager_SuperUsersPermissionsNotice'|translate }}
+ </div>
+ </div>
+
+ <div ng-if="$ctrl.activeTab === 'superuser' && $ctrl.currentUserRole == 'superuser' && !$ctrl.isAdd" class="superuser-access">
+ <p>{{:: 'UsersManager_SuperUserIntro1'|translate }}</p>
+
+ <p><strong>{{:: 'UsersManager_SuperUserIntro2'|translate }}</strong></p>
+
+ <div
+ piwik-field
+ uicontrol="checkbox"
+ name="superuser_access"
+ ng-model="$ctrl.user.superuser_access"
+ ng-attr-title="{{:: 'UsersManager_HasSuperUserAccess'|translate }}"
+ ng-click="$ctrl.confirmSuperUserChange()"
+ disabled="$ctrl.isSavingUserInfo"
+ >
+ </div>
+
+ <div class="superuser-confirm-modal modal">
+ <div class="modal-content">
+ <h2>{{:: 'UsersManager_AreYouSure'|translate }}</h2>
+ <p ng-if="!$ctrl.user.superuser_access">
+ {{:: 'UsersManager_RemoveSuperuserAccessConfirm'|translate }}
+ </p>
+ <p ng-if="$ctrl.user.superuser_access">
+ {{:: 'UsersManager_AddSuperuserAccessConfirm'|translate }}
+ </p>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.toggleSuperuserAccess()">{{:: 'General_Yes'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.user.superuser_access = !$ctrl.user.superuser_access">{{:: 'General_No'|translate }}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.js b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.js
new file mode 100644
index 0000000000..e13f594e0d
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.js
@@ -0,0 +1,140 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <piwik-user-edit-form>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikUserEditForm', {
+ templateUrl: 'plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ user: '<',
+ onDoneEditing: '&',
+ currentUserRole: '<',
+ accessLevels: '<',
+ filterAccessLevels: '<',
+ initialSiteId: '<',
+ initialSiteName: '<'
+ },
+ controller: UserEditFormController
+ });
+
+ UserEditFormController.$inject = ['$element', 'piwikApi'];
+
+ function UserEditFormController($element, piwikApi) {
+ var vm = this;
+ vm.activeTab = 'basic';
+ vm.permissionsForIdSite = 1;
+ vm.isSavingUserInfo = false;
+ vm.isPasswordChanged = false;
+ vm.userHasAccess = true;
+ vm.firstSiteAccess = null;
+ vm.isUserModified = false;
+
+ vm.$onInit = $onInit;
+ vm.$onChanges = $onChanges;
+ vm.confirmSuperUserChange = confirmSuperUserChange;
+ vm.getFormTitle = getFormTitle;
+ vm.getSaveButtonLabel = getSaveButtonLabel;
+ vm.toggleSuperuserAccess = toggleSuperuserAccess;
+ vm.saveUserInfo = saveUserInfo;
+
+ function $onInit() {
+ vm.firstSiteAccess = {
+ id: vm.initialSiteId,
+ name: vm.initialSiteName
+ };
+ }
+
+ function $onChanges() {
+ if (vm.user) {
+ vm.isAdd = false;
+ } else {
+ vm.isAdd = true;
+ vm.user = {};
+ }
+
+ if (!vm.isAdd) {
+ vm.user.password = 'XXXXXXXX'; // make sure password is not stored in the client after update/save
+ }
+ }
+
+ function getFormTitle() {
+ return vm.isAdd ? _pk_translate('UsersManager_AddNewUser') : _pk_translate('UsersManager_EditUser');
+ }
+
+ function getSaveButtonLabel() {
+ return vm.isAdd ? _pk_translate('UsersManager_CreateUser') : _pk_translate('UsersManager_SaveBasicInfo');
+ }
+
+ function confirmSuperUserChange() {
+ $element.find('.superuser-confirm-modal').openModal({ dismissible: false });
+ }
+
+ function toggleSuperuserAccess() {
+ vm.isSavingUserInfo = true;
+ piwikApi.post({
+ method: 'UsersManager.setSuperUserAccess',
+ userLogin: vm.user.login,
+ hasSuperUserAccess: vm.user.superuser_access ? '1' : '0'
+ }).catch(function () {
+ // ignore error (still displayed to user)
+ }).then(function () {
+ vm.isSavingUserInfo = false;
+ vm.isUserModified = true;
+ });
+ }
+
+ function saveUserInfo() {
+ if (vm.isAdd) {
+ createUser();
+ } else {
+ updateUser();
+ }
+ }
+
+ function createUser() {
+ vm.isSavingUserInfo = true;
+ piwikApi.post({
+ method: 'UsersManager.addUser',
+ userLogin: vm.user.login,
+ password: vm.user.password,
+ email: vm.user.email,
+ alias: vm.user.alias,
+ initialIdSite: vm.firstSiteAccess ? vm.firstSiteAccess.id : undefined
+ }).catch(function (e) {
+ vm.isSavingUserInfo = false;
+ throw e;
+ }).then(function () {
+ vm.firstSiteAccess = null;
+ vm.isSavingUserInfo = false;
+ vm.isAdd = false;
+ vm.isPasswordChanged = false;
+ vm.isUserModified = true;
+ });
+ }
+
+ function updateUser() {
+ vm.isSavingUserInfo = true;
+ piwikApi.post({
+ method: 'UsersManager.updateUser',
+ userLogin: vm.user.login,
+ password: vm.isPasswordChanged ? vm.user.password : undefined,
+ email: vm.user.email,
+ alias: vm.user.alias
+ }).catch(function (e) {
+ vm.isSavingUserInfo = false;
+ throw e;
+ }).then(function () {
+ vm.isSavingUserInfo = false;
+ vm.isPasswordChanged = false;
+ vm.isUserModified = true;
+ });
+ }
+ }
+})();
diff --git a/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.less b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.less
new file mode 100644
index 0000000000..8231e12fcb
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-edit-form/user-edit-form.component.less
@@ -0,0 +1,43 @@
+.userEditForm {
+ .entityList ul li.active a {
+ font-weight: bold;
+ }
+
+ .entityList ul {
+ .icon-warning {
+ .alert-warning;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ .user-permissions,.superuser-access {
+ margin-bottom: 32px;
+ }
+
+ .save-button-spacer {
+ height: 48px;
+ }
+
+ .superuser-confirm-modal .modal-no {
+ float: right;
+ margin-right: 1em;
+ margin-top: 1em;
+ }
+
+ .basic-info-tab {
+ .siteSelector {
+ width: calc(~'100% - 25px');
+ }
+ [piwik-siteselector] {
+ margin-bottom: 1rem;
+
+ .title {
+ &,span {
+ max-width: none;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.html b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.html
new file mode 100644
index 0000000000..fefe4e6d16
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.html
@@ -0,0 +1,152 @@
+<div class="userPermissionsEdit" ng-class="{ loading: $ctrl.isLoadingAccess }">
+ <div class="row" ng-if="!$ctrl.hasAccessToAtLeastOneSite">
+ <div piwik-notification context="warning" type="persistent" noclear="true">
+ <strong>{{:: 'General_Warning'|translate }}:</strong>
+
+ {{:: 'UsersManager_NoAccessWarning'|translate }}
+ </div>
+ </div>
+ <div class="filters row">
+ <div class="col s12 m12 l8">
+ <div class="input-field bulk-actions">
+ <a
+ class='dropdown-trigger btn'
+ href=''
+ data-activates='user-permissions-edit-bulk-actions'
+ piwik-dropdown-menu
+ ng-class="{ disabled: $ctrl.isBulkActionsDisabled }"
+ >
+ {{:: 'UsersManager_BulkActions'|translate }}
+ </a>
+ <ul id='user-permissions-edit-bulk-actions' class='dropdown-content'>
+ <li>
+ <a class='dropdown-trigger' data-activates="user-permissions-bulk-set-access" piwik-dropdown-menu>{{:: 'UsersManager_SetPermission'|translate }}</a>
+ <ul id="user-permissions-bulk-set-access" class="dropdown-content">
+ <li ng-repeat="access in $ctrl.accessLevels">
+ <a href="" ng-click="$ctrl.siteAccessToChange = null; $ctrl.roleToChangeTo = access.key; $ctrl.showChangeAccessConfirm();">{{ access.value }}</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="" ng-click="$ctrl.siteAccessToChange = null; $ctrl.roleToChangeTo = 'noaccess'; $ctrl.showRemoveAccessConfirm();">{{:: 'UsersManager_RemovePermissions'|translate }}</a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="input-field site-filter">
+ <input type="text" placeholder="Filter by website" ng-model="$ctrl.siteNameFilter" ng-model-options="{debounce: 300}" ng-change="$ctrl.offset = 0; $ctrl.fetchAccess()" />
+ </div>
+
+ <div class="input-field access-filter">
+ <div
+ piwik-field
+ uicontrol="select"
+ ng-model="$ctrl.accessLevelFilter"
+ options="$ctrl.filterAccessLevels"
+ full-width="true"
+ ng-change="$ctrl.offset = 0; $ctrl.fetchAccess()"
+ placeholder="{{ 'UsersManager_FilterByAccess'|translate }}"
+ ></div>
+ </div>
+ </div>
+
+ <div class="col s12 m12 l4 sites-for-permission-pagination-container" ng-if="$ctrl.totalEntries > $ctrl.limit">
+ <div class="sites-for-permission-pagination">
+ <a class="prev" ng-class="{ disabled: $ctrl.offset <= 0 }">
+ <span class="pointer" ng-click="$ctrl.gotoPreviousPage()">« {{:: 'General_Previous'|translate }}</span>
+ </a>
+
+ <span class="counter">
+ <span>
+ {{ 'UsersManager_XtoYofN'|translate:$ctrl.offset:$ctrl.getPaginationUpperBound():$ctrl.totalEntries }}
+ </span>
+ </span>
+
+ <a class="next" ng-class="{ disabled: $ctrl.offset + $ctrl.limit >= $ctrl.totalEntries }">
+ <span class="pointer" ng-click="$ctrl.gotoNextPage()">{{:: 'General_Next'|translate }} »</span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div piwik-notification context="info" type="persistent" noclear="true" ng-if="$ctrl.isRoleHelpToggled" class="roles-help-notification">
+ {{:: 'UsersManager_RolesHelp'|translate:"<a href='https://matomo.org/faq/general/faq_70/' target='_blank' rel='noreferrer noopener'>":"</a>":"<a href='https://matomo.org/faq/general/faq_69/' target='_blank' rel='noreferrer noopener'>":"</a>" }}
+ </div>
+
+ <table piwik-content-table id="sitesForPermission">
+ <thead>
+ <tr>
+ <th class="select-cell">
+ <span class="checkbox-container">
+ <input type="checkbox" id="perm_edit_select_all" ng-model="$ctrl.isAllCheckboxSelected" ng-change="$ctrl.onAllCheckboxChange()" />
+ <label for="perm_edit_select_all"></label>
+ </span>
+ </th>
+ <th>{{:: 'General_Name'|translate }}</th>
+ <th class="role_header">
+ <span>{{:: 'UsersManager_Role'|translate }}</span>
+ <a href="" class="helpIcon" ng-click="$ctrl.isRoleHelpToggled = !$ctrl.isRoleHelpToggled" ng-class="{ sticky: $ctrl.isRoleHelpToggled }">
+ <span class="icon-help"></span>
+ </a>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="select-all-row" ng-if="$ctrl.isAllCheckboxSelected && $ctrl.siteAccess.length < $ctrl.totalEntries">
+ <td colspan="4">
+ <div ng-if="!$ctrl.areAllResultsSelected">
+ <span piwik-translate="UsersManager_TheDisplayedWebsitesAreSelected"><strong>{{ $ctrl.siteAccess.length }}</strong></span>
+ <a href="#" ng-click="$ctrl.areAllResultsSelected = !$ctrl.areAllResultsSelected" piwik-translate="UsersManager_ClickToSelectAll"><strong>{{ $ctrl.totalEntries }}</strong></a>
+ </div>
+
+ <div ng-if="$ctrl.areAllResultsSelected">
+ <span piwik-translate="UsersManager_AllWebsitesAreSelected"><strong>{{ $ctrl.totalEntries }}</strong></span>
+ <a href="#" ng-click="$ctrl.areAllResultsSelected = !$ctrl.areAllResultsSelected" piwik-translate="UsersManager_ClickToSelectDisplayedWebsites"><strong>{{ $ctrl.siteAccess.length }}</strong></a>
+ </div>
+ </td>
+ </tr>
+ <tr ng-repeat="entry in $ctrl.siteAccess">
+ <td class="select-cell">
+ <span class="checkbox-container">
+ <input type="checkbox" ng-attr-id="perm_edit_select_row{{ $index }}" ng-model="$ctrl.selectedRows[$index]" ng-click="$ctrl.onRowSelected()" />
+ <label ng-attr-for="perm_edit_select_row{{ $index }}"></label>
+ </span>
+ </td>
+ <td>
+ <span>{{ entry.site_name }}</span>
+ </td>
+ <td>
+ <div
+ piwik-field
+ uicontrol="select"
+ ng-model="entry.role"
+ options="$ctrl.accessLevels"
+ ng-change="$ctrl.previousRole = '{{ entry.role }}'; $ctrl.roleToChangeTo = entry.role; $ctrl.siteAccessToChange = entry; $ctrl.showChangeAccessConfirm();"
+ ></div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="delete-access-confirm-modal modal">
+ <div class="modal-content">
+ <h3 ng-if="$ctrl.siteAccessToChange" piwik-translate="UsersManager_DeletePermConfirmSingle"><strong>{{ $ctrl.userLogin }}</strong>::<strong>{{ $ctrl.siteAccessToChange.site_name }}</strong></h3>
+ <p ng-if="!$ctrl.siteAccessToChange" piwik-translate="UsersManager_DeletePermConfirmMultiple"><strong>{{ $ctrl.userLogin }}</strong>::<strong>{{ $ctrl.getAffectedSitesCount() }}</strong></p>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.changeUserRole()">{{:: 'General_Yes'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.siteAccessToChange = null; $ctrl.roleToChangeTo = null;">{{:: 'General_No'|translate }}</a>
+ </div>
+ </div>
+
+ <div class="change-access-confirm-modal modal">
+ <div class="modal-content">
+ <h3 ng-if="$ctrl.siteAccessToChange" piwik-translate="UsersManager_ChangePermToSiteConfirmSingle"><strong>{{ $ctrl.userLogin }}</strong>::<strong>{{ $ctrl.siteAccessToChange.site_name }}</strong>::<strong>{{ $ctrl.getRoleDisplay($ctrl.roleToChangeTo) }}</strong></h3>
+ <p ng-if="!$ctrl.siteAccessToChange" piwik-translate="UsersManager_ChangePermToSiteConfirmMultiple"><strong>{{ $ctrl.userLogin }}</strong>::<strong>{{ $ctrl.getAffectedSitesCount() }}</strong>::<strong>{{ $ctrl.getRoleDisplay($ctrl.roleToChangeTo) }}</strong></p>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.changeUserRole()">{{:: 'General_Yes'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.siteAccessToChange.role = $ctrl.previousRole; $ctrl.siteAccessToChange = null; $ctrl.roleToChangeTo = null;">{{:: 'General_No'|translate }}</a>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.js b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.js
new file mode 100644
index 0000000000..abebcb8235
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.js
@@ -0,0 +1,262 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <piwik-user-permissions-edit>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikUserPermissionsEdit', {
+ templateUrl: 'plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ userLogin: '<',
+ limit: '<',
+ onUserHasAccessDetected: '&',
+ onAccessChange: '&',
+ accessLevels: '<',
+ filterAccessLevels: '<'
+ },
+ controller: UserPermissionsEditController
+ });
+
+ UserPermissionsEditController.$inject = ['piwikApi', '$element', '$q'];
+
+ function UserPermissionsEditController(piwikApi, $element, $q) {
+ var vm = this;
+
+ // search/pagination state
+ vm.siteAccess = [];
+ vm.offset = 0;
+ vm.totalEntries = null;
+ vm.accessLevelFilter = 'some';
+ vm.siteNameFilter = '';
+ vm.isLoadingAccess = false;
+
+ // row selection state
+ vm.isAllCheckboxSelected = false;
+ vm.selectedRows = {};
+ vm.isBulkActionsDisabled = true;
+ vm.areAllResultsSelected = false;
+ vm.previousRole = null;
+
+ // other state
+ vm.hasAccessToAtLeastOneSite = true;
+ vm.isRoleHelpToggled = false;
+
+ // intermediate state
+ vm.roleToChangeTo = null;
+ vm.siteAccessToChange = null;
+
+ vm.$onInit = $onInit;
+ vm.$onChanges = $onChanges;
+ vm.onAllCheckboxChange = onAllCheckboxChange;
+ vm.onRowSelected = onRowSelected;
+ vm.getPaginationUpperBound = getPaginationUpperBound;
+ vm.fetchAccess = fetchAccess;
+ vm.gotoPreviousPage = gotoPreviousPage;
+ vm.gotoNextPage = gotoNextPage;
+ vm.showRemoveAccessConfirm = showRemoveAccessConfirm;
+ vm.getSelectedRowsCount = getSelectedRowsCount;
+ vm.getAffectedSitesCount = getAffectedSitesCount;
+ vm.changeUserRole = changeUserRole;
+ vm.showChangeAccessConfirm = showChangeAccessConfirm;
+ vm.getRoleDisplay = getRoleDisplay;
+ vm.showAddExistingUserModal = showAddExistingUserModal;
+
+ function $onInit() {
+ vm.limit = vm.limit || 10;
+
+ resetSiteToAdd();
+ fetchAccess();
+ }
+
+ function $onChanges() {
+ vm.accessLevels = vm.accessLevels.filter(shouldShowAccessLevel);
+ vm.filterAccessLevels = vm.filterAccessLevels.filter(shouldShowAccessLevel);
+
+ if (vm.limit) {
+ fetchAccess();
+ }
+
+ function shouldShowAccessLevel(entry) {
+ return entry.key !== 'superuser';
+ }
+ }
+
+ function fetchAccess() {
+ vm.isLoadingAccess = true;
+ piwikApi.fetch({
+ method: 'UsersManager.getSitesAccessForUser',
+ limit: vm.limit,
+ offset: vm.offset,
+ filter_search: vm.siteNameFilter,
+ filter_access: vm.accessLevelFilter,
+ userLogin: vm.userLogin
+ }, { includeHeaders: true }).then(function (result) {
+ vm.isLoadingAccess = false;
+ vm.siteAccess = result.response;
+ vm.totalEntries = parseInt(result.headers('x-matomo-total-results')) || 0;
+ vm.hasAccessToAtLeastOneSite = !! result.headers('x-matomo-has-some');
+
+ if (vm.onUserHasAccessDetected) {
+ vm.onUserHasAccessDetected({ hasAccess: vm.hasAccessToAtLeastOneSite });
+ }
+
+ clearSelection();
+ }).catch(function () {
+ vm.isLoadingAccess = false;
+
+ clearSelection();
+ });
+ }
+
+ function getAllSitesInSearch() {
+ return piwikApi.fetch({
+ method: 'UsersManager.getSitesAccessForUser',
+ filter_search: vm.siteNameFilter,
+ filter_access: vm.accessLevelFilter,
+ userLogin: vm.userLogin
+ }).then(function (access) {
+ return access.map(function (a) { return a.idsite; });
+ });
+ }
+
+ function clearSelection() {
+ vm.selectedRows = {};
+ vm.areAllResultsSelected = false;
+ vm.isBulkActionsDisabled = true;
+ vm.isAllCheckboxSelected = false;
+ vm.siteAccessToChange = null;
+ }
+
+ function onAllCheckboxChange() {
+ if (!vm.isAllCheckboxSelected) {
+ clearSelection();
+ } else {
+ for (var i = 0; i !== vm.siteAccess.length; ++i) {
+ vm.selectedRows[i] = true;
+ }
+ vm.isBulkActionsDisabled = false;
+ }
+ }
+
+ function onRowSelected() {
+ var selectedRowKeyCount = getSelectedRowsCount();
+ vm.isBulkActionsDisabled = selectedRowKeyCount === 0;
+ vm.isAllCheckboxSelected = selectedRowKeyCount === vm.siteAccess.length;
+ }
+
+ function getPaginationUpperBound() {
+ return Math.min(vm.offset + vm.limit, vm.totalEntries);
+ }
+
+ function resetSiteToAdd() {
+ vm.siteToAdd = {
+ id: null,
+ name: ''
+ };
+ }
+
+ function changeUserRole() {
+ vm.isLoadingAccess = true;
+
+ return $q.resolve().then(function () {
+ if (vm.siteAccessToChange) {
+ return [vm.siteAccessToChange.idsite];
+ }
+
+ if (vm.areAllResultsSelected) {
+ return getAllSitesInSearch();
+ }
+
+ return getSelectedSites();
+ }).then(function (idSites) {
+ return piwikApi.post({
+ method: 'UsersManager.setUserAccess',
+ userLogin: vm.userLogin,
+ access: vm.roleToChangeTo,
+ 'idSites[]': idSites
+ });
+ }).catch(function () {
+ // ignore (errors will still be displayed to the user)
+ }).then(function () {
+ vm.onAccessChange();
+
+ return fetchAccess();
+ });
+ }
+
+ function getSelectedSites() {
+ var result = [];
+ Object.keys(vm.selectedRows).forEach(function (index) {
+ if (vm.selectedRows[index]
+ && vm.siteAccess[index] // safety check
+ ) {
+ result.push(vm.siteAccess[index].idsite);
+ }
+ });
+ return result;
+ }
+
+ function gotoPreviousPage() {
+ vm.offset = Math.max(0, vm.offset - vm.limit);
+
+ fetchAccess();
+ }
+
+ function gotoNextPage() {
+ var newOffset = vm.offset + vm.limit;
+ if (newOffset >= vm.totalEntries) {
+ return;
+ }
+
+ vm.offset = newOffset;
+ fetchAccess();
+ }
+
+ function showRemoveAccessConfirm() {
+ $element.find('.delete-access-confirm-modal').openModal({ dismissible: false });
+ }
+
+ function showChangeAccessConfirm() {
+ $element.find('.change-access-confirm-modal').openModal({ dismissible: false });
+ }
+
+ function showAddExistingUserModal() {
+ $element.find('.add-existing-user-modal').openModal({ dismissible: false });
+ }
+
+ function getSelectedRowsCount() {
+ var selectedRowKeyCount = 0;
+ Object.keys(vm.selectedRows).forEach(function (key) {
+ if (vm.selectedRows[key]) {
+ ++selectedRowKeyCount;
+ }
+ });
+ return selectedRowKeyCount;
+ }
+
+ function getAffectedSitesCount() {
+ if (vm.areAllResultsSelected) {
+ return vm.totalEntries;
+ }
+
+ return getSelectedRowsCount();
+ }
+
+ function getRoleDisplay(role) {
+ var result = null;
+ vm.accessLevels.forEach(function (entry) {
+ if (entry.key === role) {
+ result = entry.value;
+ }
+ });
+ return result;
+ }
+ }
+
+})();
diff --git a/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.less b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.less
new file mode 100644
index 0000000000..000d2811f6
--- /dev/null
+++ b/plugins/UsersManager/angularjs/user-permissions-edit/user-permissions-edit.component.less
@@ -0,0 +1,213 @@
+.userPermissionsEdit {
+ &.loading {
+ .sites-for-permission-pagination, table {
+ opacity: .5;
+ }
+ }
+
+ .permission-select .select-wrapper {
+ display: inline-block;
+ transform: scale(.8);
+ margin-right: -10px;
+ margin-left: -10px;
+ z-index: 999;
+
+ input {
+ margin-bottom: 0;
+ height: 1.1em;
+ line-height: 1.1em;
+ }
+
+ .caret {
+ top: 0;
+ }
+ }
+
+ .add-site {
+ float: right;
+
+ [piwik-field] {
+ display:inline-block;
+
+ .input-field {
+ width: 180px;
+ }
+ }
+
+ [piwik-siteselector] {
+ display: inline-block;
+
+ a.title {
+ width: 180px;
+ }
+
+ .siteSelector {
+ position: static !important;
+ margin-top: 1px;
+ }
+ }
+
+ .btn-flat:hover {
+ background: none;
+ }
+ }
+
+ .filters {
+ margin-left: -0.75rem;
+ margin-right: -0.75rem;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ flex-wrap: wrap;
+
+ > div:first-child {
+ flex: 1;
+ }
+
+ > div > .input-field {
+ display: inline-block;
+ vertical-align: top;
+ width: 180px;
+ }
+
+ .sites-for-permission-pagination {
+ display: inline-block;
+ vertical-align: top;
+ min-height: 2.5rem;
+ }
+
+ .form-group, .input-field, input {
+ margin: 0;
+ }
+
+ .add-site {
+ > div {
+ vertical-align: bottom;
+ margin-top: .8rem;
+ }
+
+ > a {
+ padding: 0 1rem 0 0;
+ }
+ }
+ }
+
+ .bulk-actions > a.dropdown-trigger {
+ margin-top: .8rem;
+ margin-right: 1rem;
+ }
+
+ #sitesForPermission {
+ margin-left: 0;
+ margin-right: 0;
+ width: calc(100%);
+ font-size: 100%;
+
+ td > span {
+ display:inline-block;
+ }
+
+ .select-cell {
+ width: 32px;
+ }
+
+ span.checkbox-container {
+ transform: scale(.8);
+ margin-top: -4px;
+ }
+
+ .select-wrapper {
+ transform: scale(.8) translate(-1.3rem);
+ margin-top: -0.5rem;
+ max-width: 160px;
+
+ span.caret {
+ top: 8px;
+ }
+
+ input {
+ margin-bottom: 0;
+ height: 2rem;
+ line-height: 2rem;
+ }
+ }
+
+ tr.select-all-row > td {
+ padding: 6px;
+ text-align: center;
+ }
+
+ .row.form-group {
+ margin: 0;
+ .col {
+ padding: 0;
+ }
+ }
+
+ tr .input-field {
+ margin-top: 0;
+ }
+ }
+
+ table.entityTable tbody tr td {
+ vertical-align: middle !important;
+ }
+
+ .add-permission {
+ float: right;
+ }
+
+ .sites-for-permission-pagination-container {
+ position: relative;
+ }
+
+ .sites-for-permission-pagination {
+ position: absolute;
+ bottom: 0;
+ width: calc(100%);
+ text-align: center;
+
+ a.disabled {
+ pointer-events: none;
+ color: #9e9e9e;
+ }
+
+ .counter {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+ }
+
+ .delete-site-permission {
+ float: right;
+ }
+
+ .delete-access-confirm-modal, .change-access-confirm-modal {
+ .modal-no {
+ float: right;
+ margin-right: 1em;
+ margin-top: 1em;
+ }
+ }
+
+ th.role_header {
+ .helpIcon {
+ color: #9e9e9e;
+ font-size: .8rem;
+ margin-left: .1rem;
+ text-decoration: none;
+
+ &:hover,&.sticky {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.user-permission-toast .notification {
+ padding-left: 20px;
+ &::before {
+ display:none;
+ }
+} \ No newline at end of file
diff --git a/plugins/UsersManager/angularjs/users-manager/users-manager.component.html b/plugins/UsersManager/angularjs/users-manager/users-manager.component.html
new file mode 100644
index 0000000000..9db7f9f04f
--- /dev/null
+++ b/plugins/UsersManager/angularjs/users-manager/users-manager.component.html
@@ -0,0 +1,80 @@
+<div class="usersManager">
+ <div ng-show="!$ctrl.isEditing">
+ <div piwik-content-intro>
+ <h2
+ piwik-enriched-headline
+ help-url="https://matomo.org/docs/manage-users/"
+ feature-name="Users Management"
+ >
+ {{:: 'UsersManager_ManageUsers'|translate }}
+ </h2>
+
+ <p>
+ {{:: 'UsersManager_ManageUsersDesc'|translate }}
+ </p>
+
+ <div class="row add-user-container">
+ <div class="col s12">
+ <div class="input-field">
+ <a class="btn add-new-user" ng-click="$ctrl.isEditing = true; $ctrl.userBeingEdited = null;">
+ {{:: 'UsersManager_AddUser'|translate }}
+ </a>
+ </div>
+ <div class="input-field" ng-if="$ctrl.currentUserRole !== 'superuser'">
+ <a class="btn add-existing-user" ng-click="$ctrl.showAddExistingUserModal();">
+ {{:: 'UsersManager_AddExistingUser'|translate }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <piwik-paged-users-list
+ on-edit-user="$ctrl.isEditing = true; $ctrl.userBeingEdited = user;"
+ on-change-user-role="$ctrl.onChangeUserRole(users, role)"
+ on-delete-user="$ctrl.onDeleteUser(users)"
+ on-search-change="$ctrl.searchParams = params; $ctrl.fetchUsers();"
+ initial-site-id="$ctrl.initialSiteId"
+ initial-site-name="$ctrl.initialSiteName"
+ is-loading-users="$ctrl.isLoadingUsers"
+ current-user-role="$ctrl.currentUserRole"
+ access-levels="$ctrl.accessLevels"
+ filter-access-levels="$ctrl.filterAccessLevels"
+ search-params="$ctrl.searchParams"
+ users="$ctrl.users"
+ total-entries="$ctrl.totalEntries"
+ ></piwik-paged-users-list>
+ </div>
+ </div>
+
+ <!-- TODO: whether a user is being edited should be part of the URL -->
+ <div ng-if="$ctrl.isEditing">
+ <piwik-user-edit-form
+ on-done-editing="$ctrl.onDoneEditing(isUserModified);"
+ user="$ctrl.userBeingEdited"
+ current-user-role="$ctrl.currentUserRole"
+ allow-superuser-edit="$ctrl.isCurrentUserSuperUser"
+ access-levels="$ctrl.accessLevels"
+ filter-access-levels="$ctrl.filterAccessLevels"
+ initial-site-id="$ctrl.initialSiteId"
+ initial-site-name="$ctrl.initialSiteName"
+ ></piwik-user-edit-form>
+ </div>
+
+ <div class="add-existing-user-modal modal">
+ <div class="modal-content">
+ <h3>{{:: 'UsersManager_AddExistingUser'|translate }}</h3>
+ <p>{{:: 'UsersManager_EnterUsernameOrEmail'|translate }}:</p>
+ <div
+ piwik-field
+ name="add-existing-user-email"
+ uicontrol="text"
+ ng-model="$ctrl.addNewUserLoginEmail"
+ >
+ </div>
+ </div>
+ <div class="modal-footer">
+ <a href="" class="modal-action modal-close btn" ng-click="$ctrl.addExistingUser()">{{:: 'General_Add'|translate }}</a>
+ <a href="" class="modal-action modal-close modal-no" ng-click="$ctrl.addNewUserLoginEmail = null;">{{:: 'General_Cancel'|translate }}</a>
+ </div>
+ </div>
+</div>
diff --git a/plugins/UsersManager/angularjs/users-manager/users-manager.component.js b/plugins/UsersManager/angularjs/users-manager/users-manager.component.js
new file mode 100644
index 0000000000..cc89cbed1b
--- /dev/null
+++ b/plugins/UsersManager/angularjs/users-manager/users-manager.component.js
@@ -0,0 +1,211 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <piwik-users-manager>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikUsersManager', {
+ templateUrl: 'plugins/UsersManager/angularjs/users-manager/users-manager.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ currentUserRole: '<',
+ initialSiteName: '@',
+ initialSiteId: '@',
+ accessLevels: '<',
+ filterAccessLevels: '<'
+ },
+ controller: UsersManagerController
+ });
+
+ UsersManagerController.$inject = ['$element', 'piwikApi', '$q'];
+
+ function UsersManagerController($element, piwikApi, $q) {
+ var vm = this;
+ vm.isEditing = false;
+ vm.isCurrentUserSuperUser = true;
+
+ // search state
+ vm.users = [];
+ vm.totalEntries = null;
+ vm.searchParams = {};
+ vm.isLoadingUsers = false;
+
+ vm.$onInit = $onInit;
+ vm.$onChanges = $onChanges;
+ vm.$onDestroy = $onDestroy;
+ vm.onDoneEditing = onDoneEditing;
+ vm.showAddExistingUserModal = showAddExistingUserModal;
+ vm.onChangeUserRole = onChangeUserRole;
+ vm.onDeleteUser = onDeleteUser;
+ vm.fetchUsers = fetchUsers;
+ vm.addExistingUser = addExistingUser;
+
+ function onChangeUserRole(users, role) {
+ vm.isLoadingUsers = true;
+
+ $q.resolve().then(function () {
+ if (users === 'all') {
+ return getAllUsersInSearch();
+ }
+ return users;
+ }).then(function (users) {
+ return users.filter(function (user) {
+ return user.role !== 'superuser';
+ }).map(function (user) {
+ return user.login;
+ });
+ }).then(function (userLogins) {
+ var requests = userLogins.map(function (login) {
+ return {
+ method: 'UsersManager.setUserAccess',
+ userLogin: login,
+ access: role,
+ idSites: vm.searchParams.idSite,
+ ignoreSuperusers: 1
+ };
+ });
+ return piwikApi.bulkFetch(requests, { createErrorNotification: true });
+ }).catch(function (e) {
+ // ignore (errors will still be displayed to the user)
+ }).then(function () {
+ return fetchUsers();
+ });
+ }
+
+ function onDeleteUser(users) {
+ vm.isLoadingUsers = true;
+
+ $q.resolve().then(function () {
+ if (users === 'all') {
+ return getAllUsersInSearch();
+ }
+ return users;
+ }).then(function (users) {
+ return users.map(function (user) { return user.login; });
+ }).then(function (userLogins) {
+ var requests = userLogins.map(function (login) {
+ return {
+ method: 'UsersManager.deleteUser',
+ userLogin: login
+ };
+ });
+ return piwikApi.bulkFetch(requests, { createErrorNotification: true });
+ }).catch(function () {
+ // ignore (errors will still be displayed to the user)
+ }).then(function () {
+ return fetchUsers();
+ });
+ }
+
+ function $onInit() {
+ // TODO: maybe this should go in another directive...
+ $element.tooltip({
+ track: true,
+ content: function() {
+ var title = $(this).attr('title');
+ return piwikHelper.escape(title.replace(/\n/g, '<br />'));
+ },
+ show: false,
+ hide: false
+ });
+
+ if (vm.currentUserRole === 'superuser') {
+ vm.filterAccessLevels.push({ key: 'superuser', value: 'Superuser' });
+ }
+
+ vm.searchParams = {
+ offset: 0,
+ limit: 20,
+ filter_search: '',
+ filter_access: '',
+ idSite: vm.initialSiteId
+ };
+
+ fetchUsers();
+ }
+
+ function $onChanges(changes) {
+ if (changes.limit) {
+ fetchUsers();
+ }
+ }
+
+ function $onDestroy() {
+ try {
+ $element.tooltip('destroy');
+ } catch (e) {
+ // empty
+ }
+ }
+
+ function fetchUsers() {
+ vm.isLoadingUsers = true;
+ return piwikApi.fetch($.extend({}, vm.searchParams, {
+ method: 'UsersManager.getUsersPlusRole'
+ }), { includeHeaders: true }).then(function (result) {
+ vm.totalEntries = parseInt(result.headers('x-matomo-total-results')) || 0;
+ vm.users = result.response;
+
+ vm.isLoadingUsers = false;
+ }).catch(function () {
+ vm.isLoadingUsers = false;
+ });
+ }
+
+ function getAllUsersInSearch() {
+ return piwikApi.fetch({
+ method: 'UsersManager.getUsersPlusRole',
+ filter_search: vm.searchParams.filter_search,
+ filter_access: vm.searchParams.filter_access,
+ idSite: vm.searchParams.idSite
+ });
+ }
+
+ function onDoneEditing(isUserModified) {
+ vm.isEditing = false;
+ if (isUserModified) { // if a user was modified, we must reload the users list
+ fetchUsers();
+ }
+ }
+
+ function showAddExistingUserModal() {
+ $element.find('.add-existing-user-modal').openModal({ dismissible: false });
+ }
+
+ function addExistingUser() {
+ vm.isLoadingUsers = true;
+ return piwikApi.fetch({
+ method: 'UsersManager.userExists',
+ userLogin: vm.addNewUserLoginEmail
+ }).then(function (response) {
+ if (response && response.value) {
+ return vm.addNewUserLoginEmail;
+ }
+
+ return piwikApi.fetch({
+ method: 'UsersManager.getUserLoginFromUserEmail',
+ userEmail: vm.addNewUserLoginEmail
+ }).then(function (response) {
+ return response.value;
+ });
+ }).then(function (login) {
+ return piwikApi.post({
+ method: 'UsersManager.setUserAccess',
+ userLogin: login,
+ access: 'view',
+ idSites: vm.searchParams.idSite
+ });
+ }).catch(function (error) {
+ vm.isLoadingUsers = false;
+ throw error;
+ }).then(function () {
+ return fetchUsers();
+ });
+ }
+ }
+})();
diff --git a/plugins/UsersManager/angularjs/users-manager/users-manager.component.less b/plugins/UsersManager/angularjs/users-manager/users-manager.component.less
new file mode 100644
index 0000000000..ea01698d6c
--- /dev/null
+++ b/plugins/UsersManager/angularjs/users-manager/users-manager.component.less
@@ -0,0 +1,28 @@
+.usersManager {
+ .card .card-content .card-title {
+ margin-bottom: 0;
+ }
+
+ .add-user-container {
+ &.row {
+ margin-left: -0.75rem;
+ margin-right: -0.75rem;
+ }
+
+ > .col > .input-field {
+ display: inline-block;
+ }
+ }
+
+ .add-existing-user-modal {
+ .form-group,.input-field,input.control_text {
+ margin: 0;
+ }
+
+ .modal-no {
+ float: right;
+ margin-right: 1em;
+ margin-top: 1em;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/UsersManager/lang/en.json b/plugins/UsersManager/lang/en.json
index 158c12e225..a51359a064 100644
--- a/plugins/UsersManager/lang/en.json
+++ b/plugins/UsersManager/lang/en.json
@@ -1,6 +1,11 @@
{
"UsersManager": {
"AddUser": "Add a new user",
+ "AddExistingUser": "Add an existing user",
+ "AddNewUser": "Add new user",
+ "EditUser": "Edit user",
+ "CreateUser": "Create user",
+ "SaveBasicInfo": "Save Basic Info",
"Alias": "Alias",
"AllWebsites": "All websites",
"AnonymousAccessConfirmation": "You are about to grant the anonymous user the 'view' access to this website. This means your analytics reports and your visitors information will be publicly viewable by anyone even without a login. Are you sure you want to proceed?",
@@ -18,7 +23,7 @@
"Email": "Email",
"EmailYourAdministrator": "%1$sE-mail your administrator about this problem%2$s.",
"EnterUsernameOrEmail": "Enter a username or email address",
- "ExceptionAccessValues": "The parameter access must have one of the following values: [ %s ]",
+ "ExceptionAccessValues": "The parameter access must have one of the following values: [ %1$s ], '%2$s' given.",
"ExceptionNoRoleSet": "No role is set but one of these needs to be set: %s",
"ExceptionMultipleRoleSet": "Only one role can be set but multiple have been set. Use only one of: %s",
"ExceptionAnonymousNoCapabilities": "You cannot grant any capability to the 'anonymous' user.",
@@ -34,6 +39,7 @@
"ExceptionPasswordMD5HashExpected": "UsersManager.getTokenAuth is expecting a MD5-hashed password (32 chars long string). Please call the md5() function on the password before calling this method.",
"ExceptionRemoveSuperUserAccessOnlySuperUser": "Removing the Super User access from user '%s' is not possible.",
"ExceptionSuperUserAccess": "This user has Super User access and has already permission to access and modify all websites in Matomo. You may remove the Super User access from this user and try again.",
+ "ExceptionUserHasSuperUserAccess": "The user '%s' has Super User access and has already permission to access and modify all websites in Matomo. You may remove the Super User access from this user and try again.",
"ExceptionUserDoesNotExist": "User '%s' doesn't exist.",
"ExceptionYouMustGrantSuperUserAccessFirst": "There has to be at least one user with Super User access. Please grant Super User access to another user first.",
"ExceptionUserHasViewAccessAlready": "This user has access to this website already.",
@@ -86,6 +92,49 @@
"WhenUsersAreNotLoggedInAndVisitPiwikTheyShouldAccess": "When users are not logged in and visit Matomo, they should access",
"YourUsernameCannotBeChanged": "Your username cannot be changed.",
"YourVisitsAreIgnoredOnDomain": "%1$sYour visits are ignored by Matomo on %2$s %3$s (the Matomo ignore cookie was found in your browser).",
- "YourVisitsAreNotIgnored": "%1$sYour visits are not ignored by Matomo%2$s (the Matomo ignore cookie was not found in your browser)."
+ "YourVisitsAreNotIgnored": "%1$sYour visits are not ignored by Matomo%2$s (the Matomo ignore cookie was not found in your browser).",
+ "AddUserNoInitialAccessError": "New users must be given access to a website, please set the 'initialIdSite' parameter.",
+ "AtLeastView": "At least view",
+ "ManageUsers": "Manage Users",
+ "ManageUsersDesc": "Create new users or update the existing users. You can then set their permissions here too.",
+ "NoAccessWarning": "This user has not been granted access to a website. When they login, they will see an error message. To prevent this, add access to a website below.",
+ "BulkActions": "Bulk Actions",
+ "SetPermission": "Set Permission",
+ "RemovePermissions": "Remove Permissions",
+ "RolesHelp": "Roles determine what a user can do in Matomo with regard to a specific website. Learn more about the %1$sView%2$s and %3$sAdmin%4$s roles.",
+ "Role": "Role",
+ "TheDisplayedWebsitesAreSelected": "The %1$s displayed websites are selected.",
+ "ClickToSelectAll": "Click to select all %1$s.",
+ "AllWebsitesAreSelected": "All %1$s websites are selected.",
+ "ClickToSelectDisplayedWebsites": "Click to select the %1$s displayed websites.",
+ "XtoYofN": "%1$s - %2$s of %3$s",
+ "DeletePermConfirmSingle": "Are you sure you want to remove %1$s's access to %2$s?",
+ "DeletePermConfirmMultiple": "Are you sure you want to remove %1$s's access the %2$s selected websites?",
+ "ChangePermToSiteConfirmSingle": "Are you sure you want to change %1$s's role to %2$s to %3$s?",
+ "ChangePermToSiteConfirmMultiple": "Are you sure you want to change %1$s's role to the %2$s selected websites to %3$s?",
+ "BasicInformation": "Basic Information",
+ "Permissions": "Permissions",
+ "SuperUserAccess": "Superuser Access",
+ "FirstSiteInlineHelp": "It is required to give a new user a view role for a website upon creation. If no access is given, the user will see an error when logging in. You can give more permissions after the user is created in the 'Permissions' tab that will appear on the left.",
+ "SuperUsersPermissionsNotice": "Super users have admin access to all websites, so there's no need to manage their permissions per website.",
+ "SuperUserIntro1": "Super users have the highest permissions. They can perform all administrative tasks such as adding new websites to monitor, adding users, changing user permissions, activating and deactivating plugins and even installing new plugins from the Marketplace. You can grant Super User access to other users of Divezone here.",
+ "SuperUserIntro2": "Please use this feature carefully.",
+ "HasSuperUserAccess": "Has Superuser Access",
+ "AreYouSure": "Are you sure?",
+ "RemoveSuperuserAccessConfirm": "Removing superuser access will leave the user with no permissions (you will have to add them afterwards). Do you wish to continue?",
+ "AddSuperuserAccessConfirm": "Giving a user superuser access will allow the user to have full control over Matomo and should be done sparingly. Do you wish to continue?",
+ "DeleteUsers": "Delete Users",
+ "UserSearch": "User search",
+ "FilterByAccess": "Filter by access",
+ "Username": "Username",
+ "RoleFor": "Role for",
+ "TheDisplayedUsersAreSelected": "The %1$s displayed users are selected.",
+ "AllUsersAreSelected": "All %1$s users are selected.",
+ "ClickToSelectDisplayedUsers": "Click to select the %1$s displayed users.",
+ "RemoveAllAccessToThisSite": "Remove all access to this website",
+ "DeleteUserConfirmSingle": "Are you sure you want to delete %1$s?",
+ "DeleteUserConfirmMultiple": "Are you sure you want to delete the %1$s selected users?",
+ "DeleteUserPermConfirmSingle": "Are you sure you want to change %1$s's role to %2$s for %3$s?",
+ "DeleteUserPermConfirmMultiple": "Are you sure you want to change the %1$s selected users' role to %2$s for %3$s?"
}
}
diff --git a/plugins/UsersManager/templates/index.twig b/plugins/UsersManager/templates/index.twig
index 5f2b9076dd..ca58a66426 100644
--- a/plugins/UsersManager/templates/index.twig
+++ b/plugins/UsersManager/templates/index.twig
@@ -4,313 +4,13 @@
{% block content %}
-<div piwik-content-block
- content-title="{{ title|e('html_attr') }}"
- feature="true"
- style="width:800px;"
- help-url="https://matomo.org/docs/manage-users/"
- >
-<div ng-controller="ManageUserAccessController as manageUserAccess">
- <div id="sites" class="usersManager">
- <section class="sites_selector_container">
- <p>{{ 'UsersManager_MainDescription'|translate }}</p>
-
- {% set applyAllSitesText %}
- <strong>{{ 'UsersManager_ApplyToAllWebsites'|translate }}</strong>
- {% endset %}
-
- <div piwik-siteselector
- show-selected-site="true"
- only-sites-with-admin-access="true"
- class="sites_autocomplete"
- ng-model="manageUserAccess.site"
- ng-change="manageUserAccess.siteChanged()"
- siteid="{{ idSiteSelected }}"
- sitename="{{ defaultReportSiteName }}"
- all-sites-text="{{ applyAllSitesText|raw }}"
- all-sites-location="top"
- id="usersManagerSiteSelect"
- switch-site-on-select="false"></div>
- </section>
- </div>
-
- {% block websiteAccessTable %}
-
- {% import 'ajaxMacros.twig' as ajax %}
-
- <div piwik-activity-indicator class="loadingManageUserAccess" loading="manageUserAccess.isLoading"></div>
- <div id="accessUpdated" style="vertical-align:top;"></div>
-
- {% if anonymousHasViewAccess %}
- <br/>
- <div class="alert alert-warning">
- {{ ['UsersManager_AnonymousUserHasViewAccess'|translate("'anonymous'","'view'"), 'UsersManager_AnonymousUserHasViewAccess2'|translate]|join(' ') }}
- </div>
- {% endif %}
-
- <table piwik-content-table id="manageUserAccess">
- <thead>
- <tr>
- <th class='first'>{{ 'UsersManager_User'|translate }}</th>
- <th>{{ 'UsersManager_Alias'|translate }}</th>
- <th>{{ 'UsersManager_PrivNone'|translate }}</th>
- <th>{{ 'UsersManager_PrivView'|translate }} <a href="https://matomo.org/faq/general/faq_70/" rel="noreferrer noopener" target="_blank" class="helpLink"><span class="icon-help"></span></a></th>
- <th>{{ 'UsersManager_PrivAdmin'|translate }} <a href="https://matomo.org/faq/general/faq_69/" rel="noreferrer noopener" target="_blank" class="helpLink"><span class="icon-help"></span></a></th>
- </tr>
- </thead>
-
- <tbody>
-
- {% for login,access in usersAccessByWebsite %}
-
- {% set accesValid %}
- <img src='plugins/UsersManager/images/ok.png' class='accessGranted'
- {% if access == 'noaccess' %}
- title="{{'UsersManager_UserHasNoPermission'|translate(login,'UsersManager_PrivNone'|translate,defaultReportSiteName)}}"
- {% elseif access == 'view' %}
- title="{{'UsersManager_UserHasPermission'|translate(login,'UsersManager_PrivView'|translate,defaultReportSiteName)}}"
- {% elseif access == 'admin' %}
- title="{{'UsersManager_UserHasPermission'|translate(login,'UsersManager_PrivAdmin'|translate,defaultReportSiteName)}}"
- {% endif %}
- />
- {% endset %}
- {% set superUserAccess %}<span title="{{ 'UsersManager_ExceptionSuperUserAccess'|translate }}">N/A</span>{% endset %}
-
- {% if userIsSuperUser or (hasOnlyAdminAccess and (access!='noaccess' or idSiteSelected == 'all')) %}
- <tr data-login="{{ login|e('html_attr') }}">
- <td id='login'>{{ login }}</td>
- <td>{{ usersAliasByLogin[login]|raw }}</td>
- <td id='noaccess'>
- {% if login in superUserLogins %}
- {{ superUserAccess }}
- {% elseif access=='noaccess' and idSiteSelected != 'all' %}
- {{ accesValid }}
- {% else %}
- <img src='plugins/UsersManager/images/no-access.png' class='updateAccess'
- ng-click='manageUserAccess.setAccess({{ login|json_encode}}, "noaccess")'
- title="{{'UsersManager_RemoveUserAccess'|translate(login,defaultReportSiteName)|e('html_attr')}}"
- />
- {% endif %}&nbsp;</td>
- <td id='view'>
- {% if login in superUserLogins %}
- {{ superUserAccess }}
- {% elseif access == 'view' and idSiteSelected != 'all' %}
- {{ accesValid }}
- {% else %}
- <img src='plugins/UsersManager/images/no-access.png' class='updateAccess'
- ng-click='manageUserAccess.setAccess({{ login|json_encode}}, "view")'
- title="{{'UsersManager_GiveUserAccess'|translate(login,'UsersManager_PrivView'|translate,defaultReportSiteName)|e('html_attr')}}"
- />
- {% endif %}&nbsp;</td>
- <td id='admin'>
- {% if login in superUserLogins %}
- {{ superUserAccess }}
- {% elseif login == 'anonymous' %}
- N/A
- {% else %}
- {% if access == 'admin' and idSiteSelected != 'all' %}
- {{ accesValid }}
- {% else %}
- <img src='plugins/UsersManager/images/no-access.png' class='updateAccess'
- ng-click='manageUserAccess.setAccess({{ login|json_encode}}, "admin")'
- title="{{'UsersManager_GiveUserAccess'|translate(login,'UsersManager_PrivAdmin'|translate,defaultReportSiteName)|e('html_attr')}}"
- />
- {% endif %}&nbsp;
- {% endif %}
- </td>
- </tr>
- {% endif %}
- {% endfor %}
- </tbody>
- </table>
-
- {% if hasOnlyAdminAccess %}
- <div class="tableActionBar">
- <div ng-controller="GiveUserViewAccessController as giveViewAccess" piwik-form>
- <button id="showGiveViewAccessForm"
- ng-show="!giveViewAccess.showForm" ng-click="giveViewAccess.showViewAccessForm()">
- <span class="icon-add"></span>
- {{ 'UsersManager_GiveViewAccessTitle'|translate('"' ~ defaultReportSiteName ~ '"')|raw }}
- </button>
-
- <form id="giveViewAccessForm" ng-show="giveViewAccess.showForm">
- <div piwik-field uicontrol="text" name="user_invite"
- ng-model="giveViewAccess.usernameOrEmail"
- full-width="true"
- title="{{ 'UsersManager_EnterUsernameOrEmail'|translate|e('html_attr') }}"
- >
- </div>
-
- <div piwik-save-button id="giveUserAccessToViewReports"
- onconfirm="giveViewAccess.giveAccess()"
- saving="giveViewAccess.isLoading"
- value="{{ 'UsersManager_GiveViewAccess'|translate("'" ~ defaultReportSiteName ~ "'")|e('html_attr') }}"></div>
-
- </form>
- </div>
- </div>
- <div id="ajaxErrorGiveViewAccess">
-
- </div>
- {% endif %}
-</div>
-</div>
-
-<div class="ui-confirm" id="confirm">
- <h2>{{ 'UsersManager_ChangeAllConfirm'|translate("<span class='login'></span>")|raw }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
-</div>
-
-<div class="ui-confirm" id="confirmAnonymousAccess">
- <h2>{{ 'UsersManager_AnonymousAccessConfirmation'|translate }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
-</div>
-
-{% if userIsSuperUser %}
-<div piwik-content-block content-title="{{ 'UsersManager_UsersManagement'|translate|e('html_attr') }}">
- <div class="ui-confirm" id="confirmUserRemove">
- <h2></h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
- </div>
-
- <div class="ui-confirm" id="confirmTokenRegenerate">
- <h2>{{ 'UsersManager_TokenRegenerateConfirm'|translate }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
- </div>
- <div class="ui-confirm" id="confirmTokenRegenerateSelf">
- <h2>{{ 'UsersManager_TokenRegenerateConfirmSelf'|translate }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
- </div>
-
- <br/>
- <p>{{ 'UsersManager_UsersManagementMainDescription'|translate }}
- {{ 'UsersManager_ThereAreCurrentlyNRegisteredUsers'|translate("<b>"~usersCount~"</b>")|raw }}</p>
- {% import 'ajaxMacros.twig' as ajax %}
-
- <div class="user" ng-controller="ManageUsersController as manageUsers">
- <div piwik-activity-indicator class="loadingManageUsers" loading="manageUsers.isLoading"></div>
-
- <table piwik-content-table id="users">
- <thead>
- <tr>
- <th>{{ 'General_Username'|translate }}</th>
- <th>{{ 'General_Password'|translate }}</th>
- <th>{{ 'UsersManager_Email'|translate }}</th>
- <th>{{ 'UsersManager_Alias'|translate }}</th>
- {% if showLastSeen is defined and showLastSeen %}
- <th>{{ 'UsersManager_LastSeen'|translate }}</th>
- {% endif %}
- <th>{{ 'General_Edit'|translate }}</th>
- <th>{{ 'General_Delete'|translate }}</th>
- </tr>
- </thead>
-
- <tbody>
- {% for i,user in users %}
- {% if user.login != 'anonymous' %}
- <tr class="editable" id="row{{ i }}">
- <td id="userLogin" ng-click='manageUsers.editUser("row{{ i|e('js') }}")'>{{ user.login }}</td>
- <td id="password" class="editable" ng-click='manageUsers.editUser("row{{ i|e('js') }}")'>-</td>
- <td id="email" class="editable" ng-click='manageUsers.editUser("row{{ i|e('js') }}")'>{{ user.email }}</td>
- <td id="alias" class="editable" ng-click='manageUsers.editUser("row{{ i|e('js') }}")'>{{ user.alias|raw }}</td>
- {% if user.last_seen is defined %}
- <td id="last_seen">{% if user.last_seen is empty %}-{% else %}{{ 'General_TimeAgo'|translate(user.last_seen)|raw }}{% endif %}</td>
- {% endif %}
- <td class="center">
- <button ng-click='manageUsers.editUser("row{{ i|e('js') }}")'
- class="edituser table-action" title="{{ 'General_Edit'|translate }}">
- <span class="icon-edit"></span>
- </button>
- </td>
- <td class="center">
- <button class="deleteuser table-action"
- ng-click='manageUsers.deleteUser({{ user.login|json_encode }})'
- title="{{ 'General_Delete'|translate }}">
- <span class="icon-delete"></span>
- </button>
- </td>
- </tr>
- {% endif %}
- {% endfor %}
- </tbody>
- </table>
-
- <div class="tableActionBar">
- <button class="add-user" ng-click="manageUsers.createUser()" ng-show="manageUsers.showCreateUser">
- <span class="icon-add"></span>
- {{ 'UsersManager_AddUser'|translate }}
- </button>
- </div>
- </div>
-</div>
-
-<div piwik-content-block
- id="super_user_access"
- style="width:800px;"
- content-title="{{ 'UsersManager_SuperUserAccessManagement'|translate|e('html_attr') }}">
-
- <div ng-controller="ManageSuperUserController as manageSuperUser">
-
- <p>{{ 'UsersManager_SuperUserAccessManagementMainDescription'|translate }} <br/>
- {{ 'UsersManager_SuperUserAccessManagementGrantMore'|translate }}</p>
-
- <div piwik-activity-indicator class="loadingManageSuperUser" loading="manageSuperUser.isLoading"></div>
-
- <div id="superUserAccessUpdated" style="vertical-align:top;"></div>
-
- <table piwik-content-table id="superUserAccess" >
- <thead>
- <tr>
- <th class='first'>{{ 'UsersManager_User'|translate }}</th>
- <th>{{ 'UsersManager_Alias'|translate }}</th>
- <th>{{ 'Installation_SuperUser'|translate }} <a href="https://matomo.org/faq/general/faq_35/" rel="noreferrer noopener" target="_blank" class="helpLink"><span class="icon-help"></span></a></th>
- </tr>
- </thead>
-
- <tbody>
- {% if users|length > 1 %}
- {% for login,alias in usersAliasByLogin if login != 'anonymous' %}
- <tr>
- <td id='login'>{{ login }}</td>
- <td>{{ alias|raw }}</td>
- <td id='superuser'>
- {% if login in superUserLogins %}
- <img src='plugins/UsersManager/images/ok.png' class='accessGranted'
- ng-click='manageSuperUser.removeSuperUserAccess({{ login|json_encode}})' />
- {% endif %}
- {% if not (login in superUserLogins) %}
- <img src='plugins/UsersManager/images/no-access.png' class='updateAccess'
- ng-click='manageSuperUser.giveSuperUserAccess({{ login|json_encode }})' />
- {% endif %}
- &nbsp;
- </td>
- </tr>
- {% endfor %}
- {% else %}
- <tr>
- <td colspan="3">
- {{ 'UsersManager_NoUsersExist'|translate }}
- </td>
- </tr>
- {% endif %}
- </tbody>
- </table>
-
- <div class="ui-confirm" id="superUserAccessConfirm">
- <h2> </h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
- </div>
-
- </div>
-</div>
-
-{% endif %}
-{% endblock %}
+<piwik-users-manager
+ initial-site-id="{{ idSiteSelected }}"
+ initial-site-name="{{ defaultReportSiteName }}"
+ current-user-role="'{{ currentUserRole }}'"
+ access-levels="{{ accessLevels|json_encode|e('html_attr') }}"
+ filter-access-levels="{{ filterAccessLevels|json_encode|e('html_attr') }}"
+>
+</piwik-users-manager>
{% endblock %}
diff --git a/plugins/UsersManager/tests/Fixtures/ManyUsers.php b/plugins/UsersManager/tests/Fixtures/ManyUsers.php
index b2be04550d..13a998e456 100644
--- a/plugins/UsersManager/tests/Fixtures/ManyUsers.php
+++ b/plugins/UsersManager/tests/Fixtures/ManyUsers.php
@@ -18,20 +18,55 @@ use Piwik\Tests\Framework\Fixture;
*/
class ManyUsers extends Fixture
{
+ const SITE_COUNT = 100;
+ const USER_COUNT = 100;
+
public $dateTime = '2013-01-23 01:23:45';
public $idSite = 1;
+ public $siteCopyCount;
+ public $userCopyCount;
+ public $addTextSuffixes;
- public $users = array(
- 'login1' => array(),
- 'login2' => array('view' => array(1,3,5), 'admin' => array(2,6)),
+ public $baseUsers = array(
+ 'login1' => array('superuser' => 1),
+ 'login2' => array('view' => array(3,5), 'admin' => array(1,2,6)),
'login3' => array('view' => array(), 'admin' => array()), // no access to any site
'login4' => array('view' => array(6), 'admin' => array()), // only access to one with view
'login5' => array('view' => array(), 'admin' => array(3)), // only access to one with admin
'login6' => array('view' => array(), 'admin' => array(6,3)), // access to a couple of sites with admin
'login7' => array('view' => array(2,1,6,3), 'admin' => array()), // access to a couple of sites with view
'login8' => array('view' => array(4,7), 'admin' => array(2,5)), // access to a couple of sites with admin and view
+ 'login9' => array('view' => array(5,6), 'admin' => array(8,9)),
+ 'login10' => array('superuser' => 1)
);
+ public $baseSites = [
+ 'sleep',
+ 'escapesequence',
+ 'hunter',
+ 'transistor',
+ 'wicket',
+ 'relentless',
+ 'scarecrow',
+ 'nova',
+ 'resilience',
+ 'tricks',
+ ];
+
+ public $textAdditions = [
+ 'life',
+ 'light',
+ 'flight',
+ 'conchords',
+ ];
+
+ public function __construct($userCopyCount = self::USER_COUNT, $siteCopyCount = self::SITE_COUNT, $addTextSuffixes = true)
+ {
+ $this->userCopyCount = $userCopyCount;
+ $this->siteCopyCount = $siteCopyCount;
+ $this->addTextSuffixes = $addTextSuffixes;
+ }
+
public function setUp()
{
$this->setUpWebsite();
@@ -45,28 +80,56 @@ class ManyUsers extends Fixture
private function setUpWebsite()
{
- for ($i=0; $i < 7; $i++) {
- Fixture::createWebsite('2010-01-01 00:00:00');
+ for ($i = 0; $i < self::SITE_COUNT; $i++) {
+ $siteName = $this->baseSites[$i % count($this->baseSites)];
+ if ($i != 0) {
+ $siteName .= $i;
+ }
+ Fixture::createWebsite('2010-01-01 00:00:00', $ecommerce = 0, $siteName);
}
}
protected function setUpUsers()
{
+ $totalUserCount = 0;
+
$model = new Model();
$api = API::getInstance();
- foreach ($this->users as $login => $permissions) {
- $api->addUser($login, 'password', $login . '@example.com');
- foreach ($permissions as $access => $idSites) {
- if (!empty($idSites)) {
- $api->setUserAccess($login, $access, $idSites);
+ for ($i = 0; $i != $this->userCopyCount; ++$i) {
+ $addToEmail = $i % 2 == 0;
+
+ foreach ($this->baseUsers as $baseLogin => $permissions) {
+ ++$totalUserCount;
+
+ $textAddition = $this->textAdditions[$totalUserCount % count($this->textAdditions)];
+
+ $login = $this->addTextSuffixes ? ($i . '_' . $baseLogin) : $baseLogin;
+ if ($this->addTextSuffixes && !$addToEmail) {
+ $login .= $textAddition;
}
- }
- $user = $model->getUser($login);
- $this->users[$login]['token'] = $user['token_auth'];
- }
+ $email = $login . '@example.com';
+ if ($this->addTextSuffixes &&$addToEmail) {
+ $email = $login . $textAddition . '@example.com';
+ }
- $api->setSuperUserAccess('login1', true);
- }
+ $api->addUser($login, 'password', $email);
+
+ foreach ($permissions as $access => $idSites) {
+ if (empty($idSites)) {
+ continue;
+ }
+ if ($access == 'superuser') {
+ $api->setSuperUserAccess($login, true);
+ } else {
+ $api->setUserAccess($login, $access, $idSites);
+ }
+ }
+
+ $user = $model->getUser($login);
+ $this->users[$login]['token'] = $user['token_auth'];
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/plugins/UsersManager/tests/Integration/APITest.php b/plugins/UsersManager/tests/Integration/APITest.php
index 4c23d5f7f0..3f19c2970a 100644
--- a/plugins/UsersManager/tests/Integration/APITest.php
+++ b/plugins/UsersManager/tests/Integration/APITest.php
@@ -11,8 +11,10 @@ namespace Piwik\Plugins\UsersManager\tests;
use Piwik\Access\Role\View;
use Piwik\Access\Role\Write;
use Piwik\Auth\Password;
+use Piwik\Container\StaticContainer;
use Piwik\Option;
use Piwik\Piwik;
+use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
use Piwik\Plugins\UsersManager\API;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\UsersManager;
@@ -143,6 +145,7 @@ class APITest extends IntegrationTestCase
$this->api = API::getInstance();
$this->model = new Model();
+ FakeAccess::clearAccess();
FakeAccess::$superUser = true;
Fixture::createWebsite('2014-01-01 00:00:00');
@@ -290,7 +293,8 @@ class APITest extends IntegrationTestCase
{
$this->api->updateUser($this->login, 'newPassword', 'email@example.com', 'newAlias', false);
- $user = $this->api->getUser($this->login);
+ $model = new Model();
+ $user = $model->getUser($this->login);
$this->assertSame('email@example.com', $user['email']);
$this->assertSame('newAlias', $user['alias']);
@@ -330,6 +334,290 @@ class APITest extends IntegrationTestCase
$this->assertEquals($expected, $access);
}
+ public function test_getUsersPlusRole_shouldReturnSelfIfUserDoesNotHaveAdminAccessToSite()
+ {
+ $this->addUserWithAccess('userLogin2', 'view', 1);
+ $this->setCurrentUser('userLogin2', 'view', 1);
+
+ $users = $this->api->getUsersPlusRole(1);
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldNotAllowSuperuserFilter_ifUserIsNotSuperUser()
+ {
+ $this->addUserWithAccess('userLogin2', 'view', 1);
+ $this->addUserWithAccess('userLogin3', 'superuser', 1);
+ $this->setCurrentUser('userLogin2', 'view', 1);
+
+ $users = $this->api->getUsersPlusRole(1, null, null, null, 'superuser');
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldReturnAllUsersAndAccess_ifUserHasAdminAccess()
+ {
+ $this->addUserWithAccess('userLogin2', 'admin', 1);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'admin', 1);
+ $this->addUserWithAccess('userLogin5', null, 1);
+ $this->setCurrentUser('userLogin2', 'admin', 1);
+
+ $users = $this->api->getUsersPlusRole(1);
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'admin', 'capabilities' => []],
+ ['login' => 'userLogin3', 'alias' => 'userLogin3', 'role' => 'view', 'capabilities' => []],
+ ['login' => 'userLogin4', 'alias' => 'userLogin4', 'role' => 'admin', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldLimitUsersReturnedToThoseWithAccessToSitesAsCurrentUsersAdminSites_IfCurrentUserIsAdmin()
+ {
+ $this->addUserWithAccess('userLogin2', 'admin', [1, 2]);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'admin', 1);
+ $this->addUserWithAccess('userLogin5', null, [1, 2]);
+ $this->api->setUserAccess('userLogin5', 'view', 2);
+ $this->setCurrentUser('userLogin2', 'admin', [1, 2]);
+
+ $users = $this->api->getUsersPlusRole(1);
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'admin', 'capabilities' => []],
+ ['login' => 'userLogin3', 'alias' => 'userLogin3', 'role' => 'view', 'capabilities' => []],
+ ['login' => 'userLogin4', 'alias' => 'userLogin4', 'role' => 'admin', 'capabilities' => []],
+ ['login' => 'userLogin5', 'alias' => 'userLogin5', 'role' => 'noaccess', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldReturnAllUsersAndAccess_ifUserHasSuperuserAccess()
+ {
+ $this->addUserWithAccess('userLogin2', 'superuser', 1);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'superuser', 1);
+ $this->addUserWithAccess('userLogin5', null, 1);
+ $this->setCurrentUser('userLogin2', 'superuser', 1);
+
+ $users = $this->api->getUsersPlusRole(1);
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin', 'alias' => 'userLogin', 'email' => 'userlogin@password.de', 'superuser_access' => false, 'role' => 'noaccess', 'capabilities' => []],
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'email' => 'userLogin2@password.de', 'superuser_access' => true, 'role' => 'superuser', 'capabilities' => []],
+ ['login' => 'userLogin3', 'alias' => 'userLogin3', 'email' => 'userLogin3@password.de', 'superuser_access' => false, 'role' => 'view', 'capabilities' => []],
+ ['login' => 'userLogin4', 'alias' => 'userLogin4', 'email' => 'userLogin4@password.de', 'superuser_access' => true, 'role' => 'superuser', 'capabilities' => []],
+ ['login' => 'userLogin5', 'alias' => 'userLogin5', 'email' => 'userLogin5@password.de', 'superuser_access' => false, 'role' => 'noaccess', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldFilterUsersByAccessCorrectly()
+ {
+ $this->addUserWithAccess('userLogin2', 'admin', 1);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'superuser', 1);
+ $this->addUserWithAccess('userLogin5', 'admin', 1);
+ $this->setCurrentUser('userLogin2', 'admin', 1);
+
+ $users = $this->api->getUsersPlusRole(1, null, null, null, 'admin');
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'admin', 'capabilities' => []],
+ ['login' => 'userLogin5', 'alias' => 'userLogin5', 'role' => 'admin', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldReturnUsersWithNoAccessCorrectly()
+ {
+ $this->addUserWithAccess('userLogin2', 'noaccess', 1);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'superuser', 1);
+ $this->addUserWithAccess('userLogin5', 'noaccess', 1);
+
+ $users = $this->api->getUsersPlusRole(1, null, null, null, 'noaccess');
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin', 'alias' => 'userLogin', 'role' => 'noaccess', 'superuser_access' => false, 'email' => 'userlogin@password.de', 'capabilities' => []],
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'role' => 'noaccess', 'superuser_access' => false, 'email' => 'userLogin2@password.de', 'capabilities' => []],
+ ['login' => 'userLogin5', 'alias' => 'userLogin5', 'role' => 'noaccess', 'superuser_access' => false, 'email' => 'userLogin5@password.de', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldSearchForSuperUsersCorrectly()
+ {
+ $this->addUserWithAccess('userLogin2', 'admin', 1);
+ $this->api->setSuperUserAccess('userLogin2', true);
+ $this->addUserWithAccess('userLogin3', 'view', 1);
+ $this->addUserWithAccess('userLogin4', 'superuser', 1);
+ $this->addUserWithAccess('userLogin5', null, 1);
+ $this->setCurrentUser('userLogin2', 'superuser', 1);
+
+ $users = $this->api->getUsersPlusRole(1, null, null, null, 'superuser');
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'email' => 'userLogin2@password.de', 'superuser_access' => true, 'role' => 'superuser', 'capabilities' => []],
+ ['login' => 'userLogin4', 'alias' => 'userLogin4', 'email' => 'userLogin4@password.de', 'superuser_access' => true, 'role' => 'superuser', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldSearchByTextCorrectly()
+ {
+ $this->addUserWithAccess('searchTextLogin', 'superuser', 1, 'someemail@email.com', 'alias');
+ $this->addUserWithAccess('userLogin2', 'view', 1, 'searchTextdef@email.com');
+ $this->addUserWithAccess('userLogin3', 'superuser', 1, 'someemail2@email.com', 'alias-searchTextABC');
+ $this->addUserWithAccess('userLogin4', null, 1);
+ $this->setCurrentUser('searchTextLogin', 'superuser', 1);
+
+ $users = $this->api->getUsersPlusRole(1, null, null, 'searchText');
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'searchTextLogin', 'alias' => 'alias', 'email' => 'someemail@email.com', 'superuser_access' => true, 'role' => 'superuser', 'capabilities' => []],
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'email' => 'searchTextdef@email.com', 'superuser_access' => false, 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getUsersPlusRole_shouldApplyLimitAndOffsetCorrectly()
+ {
+ $this->addUserWithAccess('searchTextLogin', 'superuser', 1, 'someemail@email.com');
+ $this->addUserWithAccess('userLogin2', 'view', 1, 'searchTextdef@email.com');
+ $this->addUserWithAccess('userLogin3', 'superuser', 1, 'someemail2@email.com', 'alias-searchTextABC');
+ $this->addUserWithAccess('userLogin4', null, 1);
+ $this->setCurrentUser('searchTextLogin', 'superuser', 1);
+
+ $users = $this->api->getUsersPlusRole(1, $limit = 2, $offset = 1);
+ $this->cleanUsers($users);
+ $expected = [
+ ['login' => 'userLogin', 'alias' => 'userLogin', 'email' => 'userlogin@password.de', 'superuser_access' => false, 'role' => 'noaccess', 'capabilities' => []],
+ ['login' => 'userLogin2', 'alias' => 'userLogin2', 'email' => 'searchTextdef@email.com', 'superuser_access' => false, 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $users);
+ }
+
+ public function test_getSitesAccessForUser_shouldReturnAccessForUser()
+ {
+ $this->api->setUserAccess('userLogin', 'admin', [1]);
+ $this->api->setUserAccess('userLogin', 'view', [2]);
+ $this->api->setUserAccess('userLogin', 'view', [3]);
+
+ $access = $this->api->getSitesAccessForUser('userLogin');
+ $expected = [
+ ['idsite' => '1', 'site_name' => 'Piwik test', 'role' => 'admin', 'capabilities' => []],
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '3', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldApplyLimitAndOffsetCorrectly()
+ {
+ $this->api->setUserAccess('userLogin', 'admin', [1]);
+ $this->api->setUserAccess('userLogin', 'view', [2]);
+ $this->api->setUserAccess('userLogin', 'view', [3]);
+
+ $access = $this->api->getSitesAccessForUser('userLogin', $limit = 2, $offset = 1);
+ $expected = [
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '3', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldSearchSitesCorrectly()
+ {
+ Fixture::createWebsite('2010-01-02 00:00:00');
+
+ $this->api->setUserAccess('userLogin', 'admin', [1]);
+ $this->api->setUserAccess('userLogin', 'view', [2]);
+ $this->api->setUserAccess('userLogin', 'view', [3]);
+ $this->api->setUserAccess('userLogin', 'view', [4]);
+
+ SitesManagerAPI::getInstance()->updateSite(1, 'searchTerm site');
+ SitesManagerAPI::getInstance()->updateSite(2, null, ['http://searchTerm.com']);
+ SitesManagerAPI::getInstance()->updateSite(3, null, null, null, null, null, null, null, null, null, null, 'the searchTerm group');
+
+ $access = $this->api->getSitesAccessForUser('userLogin', null, null, 'searchTerm');
+ $expected = [
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '3', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '1', 'site_name' => 'searchTerm site', 'role' => 'admin', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldFilterByAccessCorrectly()
+ {
+ $this->api->setUserAccess('userLogin', 'admin', [1]);
+ $this->api->setUserAccess('userLogin', 'view', [2]);
+ $this->api->setUserAccess('userLogin', 'view', [3]);
+
+ $access = $this->api->getSitesAccessForUser('userLogin', null, null, null, 'view');
+ $expected = [
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '3', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldLimitSitesIfUserIsAdmin()
+ {
+ $this->addUserWithAccess('userLogin2', 'view', [1, 2, 3], 'userlogin2@email.com');
+
+ $this->api->setUserAccess('userLogin', 'admin', [1, 2]);
+ $this->api->setUserAccess('userLogin', 'view', [3]);
+
+ $this->setCurrentUser('userLogin', 'admin', [1, 2]);
+
+ $access = $this->api->getSitesAccessForUser('userLogin2', null, null, null, 'view');
+ $expected = [
+ ['idsite' => '1', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldSelectSitesCorrectlyIfAtLeastViewRequested()
+ {
+ $this->addUserWithAccess('userLogin2', 'view', [1], 'userlogin2@email.com');
+ $this->api->setUserAccess('userLogin2', 'admin', [2]);
+
+ $access = $this->api->getSitesAccessForUser('userLogin2', null, null, null, 'some');
+ $expected = [
+ ['idsite' => '1', 'site_name' => 'Piwik test', 'role' => 'view', 'capabilities' => []],
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'admin', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+ }
+
+ public function test_getSitesAccessForUser_shouldReportIfUserHasNoAccessToSites()
+ {
+ $access = $this->api->getSitesAccessForUser('userLogin');
+ $expected = [
+ ['idsite' => '1', 'site_name' => 'Piwik test', 'role' => 'noaccess', 'capabilities' => []],
+ ['idsite' => '2', 'site_name' => 'Piwik test', 'role' => 'noaccess', 'capabilities' => []],
+ ['idsite' => '3', 'site_name' => 'Piwik test', 'role' => 'noaccess', 'capabilities' => []],
+ ];
+ $this->assertEquals($expected, $access);
+
+ // test when search returns empty result
+ $this->api->setUserAccess('userLogin', 'view', 1);
+
+ $access = $this->api->getSitesAccessForUser('userLogin', null, null, 'asdklfjds');
+ $expected = [];
+ $this->assertEquals($expected, $access);
+ }
+
/**
* @expectedException \Exception
* @expectedExceptionMessage UsersManager_ExceptionMultipleRoleSet
@@ -605,4 +893,32 @@ class APITest extends IntegrationTestCase
]),
);
}
+
+ private function addUserWithAccess($username, $accessLevel, $idSite, $email = null, $alias = null)
+ {
+ $this->api->addUser($username, 'password', $email ?: "$username@password.de", $alias);
+ if ($accessLevel == 'superuser') {
+ $this->api->setSuperUserAccess($username, true);
+ } else if ($accessLevel) {
+ $this->api->setUserAccess($username, $accessLevel, $idSite);
+ }
+ }
+
+ public function setCurrentUser($username, $accessLevel, $idSite)
+ {
+ FakeAccess::$identity = $username;
+ FakeAccess::$superUser = $accessLevel == 'superuser';
+ if ($accessLevel == 'view') {
+ FakeAccess::$idSitesView = is_array($idSite) ? $idSite : [$idSite];
+ } else if ($accessLevel == 'admin') {
+ FakeAccess::$idSitesAdmin = is_array($idSite) ? $idSite : [$idSite];
+ }
+ }
+
+ private function cleanUsers(&$users)
+ {
+ foreach ($users as &$user) {
+ unset($user['date_registered']);
+ }
+ }
}
diff --git a/plugins/UsersManager/tests/Integration/UsersManagerTest.php b/plugins/UsersManager/tests/Integration/UsersManagerTest.php
index 8bc80b436a..af70b58a92 100644
--- a/plugins/UsersManager/tests/Integration/UsersManagerTest.php
+++ b/plugins/UsersManager/tests/Integration/UsersManagerTest.php
@@ -14,6 +14,7 @@ use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Plugins\UsersManager\API;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\UsersManager;
+use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\Mock\FakeAccess;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
use Exception;
@@ -77,7 +78,7 @@ class UsersManagerTest extends IntegrationTestCase
$newAlias = $user['alias'];
}
- $userAfter = $this->api->getUser($user["login"]);
+ $userAfter = $this->model->getUser($user["login"]);
$this->assertArrayHasKey('date_registered', $userAfter);
$this->assertRegExp(self::DATETIME_REGEX, $userAfter['date_registered']);
@@ -253,7 +254,7 @@ class UsersManagerTest extends IntegrationTestCase
$time = time();
$this->api->addUser($login, $password, $email, $alias);
- $user = $this->api->getUser($login);
+ $user = $this->model->getUser($login);
// check that the date registered is correct
$this->assertTrue($time <= strtotime($user['date_registered']) && strtotime($user['date_registered']) <= time(),
@@ -279,11 +280,62 @@ class UsersManagerTest extends IntegrationTestCase
$this->assertTrue($passwordHelper->verify(UsersManager::getPasswordHash($password), $user['password']));
}
+ public function test_addUser_shouldAllowAdminUsersToCreateUsers()
+ {
+ FakeAccess::$superUser = false;
+ FakeAccess::$idSitesAdmin = [1];
+
+ $login = "geggeq55eqag";
+ $password = "mypassword";
+ $email = "mgeag4544i@geq.com";
+ $alias = "her is my alias )(&|\" '£%*(&%+))";
+
+ $this->api->addUser($login, $password, $email, $alias, false, 1);
+
+ FakeAccess::$superUser = true;
+ $user = $this->api->getUser($login);
+
+ $this->assertEquals($login, $user['login']);
+ $this->assertEquals($email, $user['email']);
+ $this->assertEquals($alias, $user['alias']);
+
+ FakeAccess::$superUser = true;
+
+ $access = $this->api->getSitesAccessFromUser($login);
+ $this->assertEquals([
+ ['site' => 1, 'access' => 'view'],
+ ], $access);
+ }
+
/**
* @expectedException \Exception
- * @expectedExceptionMessage UsersManager_ExceptionDeleteDoesNotExist
+ * @expectedExceptionMessage UsersManager_AddUserNoInitialAccessError
*/
- public function testSeleteUserDoesntExist()
+ public function test_addUser_shouldNotAllowAdminUsersToCreateUsers_WithNoInitialSiteWithAccess()
+ {
+ FakeAccess::$superUser = false;
+ FakeAccess::$idSitesAdmin = [1];
+
+ $this->api->addUser('userLogin2', 'password', 'userlogin2@email.com', 'userLogin2');
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage checkUserHasAdminAccess Fake exception
+ */
+ public function test_addUser_shouldNotAllowAdminUsersToCreateUsersWithAccessToSite_ThatAdminUserDoesNotHaveAccessTo()
+ {
+ FakeAccess::$superUser = false;
+ FakeAccess::$idSitesAdmin = [2];
+
+ $this->api->addUser('userLogin2', 'password', 'userlogin2@email.com', 'userLogin2', false, 1);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage UsersManager_ExceptionUserDoesNotExist
+ */
+ public function testDeleteUserDoesntExist()
{
$this->api->addUser("geggeqgeqag", "geqgeagae", "test@test.com", "alias");
$this->api->deleteUser("geggeqggnew");
@@ -291,7 +343,7 @@ class UsersManagerTest extends IntegrationTestCase
/**
* @expectedException \Exception
- * @expectedExceptionMessage UsersManager_ExceptionDeleteDoesNotExist
+ * @expectedExceptionMessage UsersManager_ExceptionUserDoesNotExist
*/
public function testDeleteUserEmptyUser()
{
@@ -300,7 +352,7 @@ class UsersManagerTest extends IntegrationTestCase
/**
* @expectedException \Exception
- * @expectedExceptionMessage UsersManager_ExceptionDeleteDoesNotExist
+ * @expectedExceptionMessage UsersManager_ExceptionUserDoesNotExist
*/
public function testDeleteUserNullUser()
{
@@ -327,6 +379,8 @@ class UsersManagerTest extends IntegrationTestCase
*/
public function testDeleteUser()
{
+ Fixture::createSuperUser();
+
$this->addSites(3);
//add user and set some rights
@@ -377,7 +431,7 @@ class UsersManagerTest extends IntegrationTestCase
$alias = "";
$this->api->addUser($login, $password, $email, $alias);
- $user = $this->api->getUser($login);
+ $user = $this->model->getUser($login);
// check that all fields are the same
$this->assertEquals($login, $user['login']);
@@ -533,7 +587,7 @@ class UsersManagerTest extends IntegrationTestCase
/**
* @expectedException \Exception
- * @expectedExceptionMessage UsersManager_ExceptionSuperUserAccess
+ * @expectedExceptionMessage UsersManager_ExceptionUserHasSuperUserAccess
*/
public function testSetUserAccess_ShouldFail_IfLoginIsUserWithSuperUserAccess()
{
@@ -1039,4 +1093,19 @@ class UsersManagerTest extends IntegrationTestCase
'Piwik\Access' => new FakeAccess()
);
}
+
+ private function assertUserNotExists($login)
+ {
+ try {
+ $this->api->getUser($login);
+ $this->fail("User $login still exists!");
+ } catch (Exception $expected) {
+ $this->assertRegExp("(UsersManager_ExceptionUserDoesNotExist)", $expected->getMessage());
+ }
+ }
+
+ public function testName()
+ {
+
+ }
}
diff --git a/plugins/UsersManager/tests/System/ApiTest.php b/plugins/UsersManager/tests/System/ApiTest.php
index 8c1baaec80..ca79683ca3 100644
--- a/plugins/UsersManager/tests/System/ApiTest.php
+++ b/plugins/UsersManager/tests/System/ApiTest.php
@@ -74,4 +74,4 @@ class ApiTest extends SystemTestCase
}
-ApiTest::$fixture = new ManyUsers(); \ No newline at end of file
+ApiTest::$fixture = new ManyUsers(1, 1, false); \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login1_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login1_when_superuseraccess.xml
index 4b1a43a0ee..c54ea7c443 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login1_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login1_when_superuseraccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login1</login>
-
<alias>login1</alias>
<email>login1@example.com</email>
<superuser_access>1</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_adminaccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_adminaccess.xml
index b394489b94..4356dcc2c8 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_adminaccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_adminaccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login2</login>
-
<alias>login2</alias>
<email>login2@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_superuseraccess.xml
index b394489b94..4356dcc2c8 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login2_when_superuseraccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login2</login>
-
<alias>login2</alias>
<email>login2@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_superuseraccess.xml
index caa6b7267e..6d73ea60cb 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_superuseraccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login4</login>
-
<alias>login4</alias>
<email>login4@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_viewaccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_viewaccess.xml
index caa6b7267e..6d73ea60cb 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_viewaccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login4_when_viewaccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login4</login>
-
<alias>login4</alias>
<email>login4@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login6_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login6_when_superuseraccess.xml
index beeca3f033..99cd8e9dc2 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login6_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUser_login6_when_superuseraccess.xml
@@ -2,11 +2,9 @@
<result>
<row>
<login>login6</login>
-
<alias>login6</alias>
<email>login6@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_adminaccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_adminaccess.xml
index ca45c6e1b2..f3c704a143 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_adminaccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_adminaccess.xml
@@ -5,5 +5,6 @@
<login4>view</login4>
<login6>admin</login6>
<login7>view</login7>
+ <login9>view</login9>
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_superuseraccess.xml
index ca45c6e1b2..f3c704a143 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersAccessFromSite_6_when_superuseraccess.xml
@@ -5,5 +5,6 @@
<login4>view</login4>
<login6>admin</login6>
<login7>view</login7>
+ <login9>view</login9>
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_adminaccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_adminaccess.xml
index 11c32f1705..7195342606 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_adminaccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_adminaccess.xml
@@ -5,4 +5,5 @@
<row>login6</row>
<row>login7</row>
<row>login8</row>
+ <row>login9</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_superuseraccess.xml
index 151105d5b4..95b0905574 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersLogin__when_superuseraccess.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
<row>login1</row>
+ <row>login10</row>
<row>login2</row>
<row>login3</row>
<row>login4</row>
@@ -8,5 +9,6 @@
<row>login6</row>
<row>login7</row>
<row>login8</row>
+ <row>login9</row>
<row>superUserLogin</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersSitesFromAccess_admin_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersSitesFromAccess_admin_when_superuseraccess.xml
index 52d3c05844..3bb05746e6 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersSitesFromAccess_admin_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersSitesFromAccess_admin_when_superuseraccess.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
<login2>
+ <row>1</row>
<row>2</row>
<row>6</row>
</login2>
@@ -15,4 +16,8 @@
<row>2</row>
<row>5</row>
</login8>
+ <login9>
+ <row>8</row>
+ <row>9</row>
+ </login9>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersWithSiteAccess_3_admin_when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersWithSiteAccess_3_admin_when_superuseraccess.xml
index df237acece..749c6167be 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersWithSiteAccess_3_admin_when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsersWithSiteAccess_3_admin_when_superuseraccess.xml
@@ -2,20 +2,16 @@
<result>
<row>
<login>login5</login>
-
<alias>login5</alias>
<email>login5@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login6</login>
-
<alias>login6</alias>
<email>login6@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_adminaccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_adminaccess.xml
index 30411b3ef8..88daa22577 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_adminaccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_adminaccess.xml
@@ -20,4 +20,8 @@
<login>login8</login>
<alias>login8</alias>
</row>
+ <row>
+ <login>login9</login>
+ <alias>login9</alias>
+ </row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_superuseraccess.xml b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_superuseraccess.xml
index 35351a9999..b705a46a92 100644
--- a/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_superuseraccess.xml
+++ b/plugins/UsersManager/tests/System/expected/test___UsersManager.getUsers__when_superuseraccess.xml
@@ -2,83 +2,79 @@
<result>
<row>
<login>login1</login>
-
<alias>login1</alias>
<email>login1@example.com</email>
<superuser_access>1</superuser_access>
+ </row>
+ <row>
+ <login>login10</login>
+ <alias>login10</alias>
+ <email>login10@example.com</email>
+ <superuser_access>1</superuser_access>
</row>
<row>
<login>login2</login>
-
<alias>login2</alias>
<email>login2@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login3</login>
-
<alias>login3</alias>
<email>login3@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login4</login>
-
<alias>login4</alias>
<email>login4@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login5</login>
-
<alias>login5</alias>
<email>login5@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login6</login>
-
<alias>login6</alias>
<email>login6@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login7</login>
-
<alias>login7</alias>
<email>login7@example.com</email>
<superuser_access>0</superuser_access>
-
</row>
<row>
<login>login8</login>
-
<alias>login8</alias>
<email>login8@example.com</email>
<superuser_access>0</superuser_access>
+ </row>
+ <row>
+ <login>login9</login>
+ <alias>login9</alias>
+ <email>login9@example.com</email>
+ <superuser_access>0</superuser_access>
</row>
<row>
<login>superUserLogin</login>
-
<alias>superUserLogin</alias>
<email>hello@example.org</email>
<superuser_access>1</superuser_access>
-
</row>
</result> \ No newline at end of file
diff --git a/plugins/UsersManager/tests/UI/UsersManager_spec.js b/plugins/UsersManager/tests/UI/UsersManager_spec.js
index a71585d776..77ada45f80 100644
--- a/plugins/UsersManager/tests/UI/UsersManager_spec.js
+++ b/plugins/UsersManager/tests/UI/UsersManager_spec.js
@@ -13,98 +13,462 @@ describe("UsersManager", function () {
var url = "?module=UsersManager&action=index";
- function assertScreenshotEquals(screenshotName, done, test)
- {
- expect.screenshot(screenshotName).to.be.captureSelector('#content', test, done);
- }
-
- function openGiveAccessForm(page) {
- page.click('#showGiveViewAccessForm');
- }
-
- function setLoginOrEmailForGiveAccessForm(page, loginOrEmail)
- {
- page.evaluate(function () {
- $('#user_invite').val('');
- });
- page.sendKeys('#user_invite', loginOrEmail);
- }
+ it('should display the manage users page correctly', function (done) {
+ expect.screenshot("load").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.load(url);
+ }, done);
+ });
- function submitGiveAccessForm(page)
- {
- page.click('#giveUserAccessToViewReports');
- page.wait(1000); // we wait in case error notification is still fading in and not fully visible yet
- }
+ it('should change the results page when next is clicked', function (done) {
+ expect.screenshot("next_click").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
- before(function () {
- testEnvironment.idSitesAdminAccess = [1,2];
- testEnvironment.save();
+ page.click('.usersListPagination .btn.next');
+ }, done);
});
- after(function () {
- delete testEnvironment.idSitesAdminAccess;
- testEnvironment.save();
+ it('should filter by username and access level when the inputs are filled', function (done) {
+ expect.screenshot("filters").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.sendKeys('#user-text-filter', 'ight');
+ page.evaluate(function () {
+ $('select[name=access-level-filter]').val('string:view').change();
+ });
+ }, done);
});
- it("should show only users having access to same site", function (done) {
- assertScreenshotEquals("loaded_as_admin", done, function (page) {
- page.load(url);
- });
+ it('should display access for a different site when the roles for select is changed', function (done) {
+ expect.screenshot("role_for").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ // remove access filter
+ page.evaluate(function () {
+ $('select[name=access-level-filter]').val('string:').change();
+ });
+
+ page.click('th.role_header .siteSelector a.title');
+ page.click('.siteSelector .custom_select_container a:contains(relentless)');
+ }, done);
});
- it("should open give view access form when clicking on button", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_form_opened", done, function (page) {
- openGiveAccessForm(page);
- });
+ it('should select rows when individual row select is clicked', function (done) {
+ expect.screenshot("rows_selected").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('td.select-cell label:eq(0)');
+ page.click('td.select-cell label:eq(3)');
+ page.click('td.select-cell label:eq(8)');
+ }, done);
});
- it("should show an error when nothing entered", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_no_user_entered", done, function (page) {
- submitGiveAccessForm(page);
- });
+ it('should select all rows when all row select is clicked', function (done) {
+ expect.screenshot("all_rows_selected").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('th.select-cell label');
+ }, done);
});
- it("should show an error when no such user found", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_user_not_found", done, function (page) {
- setLoginOrEmailForGiveAccessForm(page, 'anyNoNExistingUser');
- submitGiveAccessForm(page);
- });
+ it('should select all rows in search when link in table is clicked', function (done) {
+ expect.screenshot("all_rows_in_search").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.toggle-select-all-in-search');
+ page.sendMouseEvent('mousemove', { x: 0, y: 0 });
+ }, done);
});
- it("should show an error if user already has access", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_user_already_has_access", done, function (page) {
- setLoginOrEmailForGiveAccessForm(page, 'login2');
- submitGiveAccessForm(page);
- });
+ it('should deselect all rows in search except for displayed rows when link in table is clicked again', function (done) {
+ expect.screenshot("all_rows_selected").to.be.captureSelector('all_rows_in_search_deselected', '.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.toggle-select-all-in-search');
+ page.sendMouseEvent('mousemove', { x: 0, y: 0 });
+ }, done);
});
- it("should add a user by login", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_via_login", done, function (page) {
- setLoginOrEmailForGiveAccessForm(page, 'login3');
- submitGiveAccessForm(page);
- });
+ it('should show bulk action confirm when bulk change access option used', function (done) {
+ expect.screenshot("bulk_set_access_confirm").to.be.captureSelector('.change-user-role-confirm-modal', function (page) {
+ page.setViewportSize(1250);
+
+ // remove filters
+ page.evaluate(function () {
+ $('select[name=access-level-filter]').val('string:').change();
+ });
+
+ page.click('.toggle-select-all-in-search'); // reselect all in search
+
+ page.click('.bulk-actions.btn');
+ page.mouseMove('#user-list-bulk-actions>li:first');
+ page.click('#bulk-set-access a:contains(Admin)');
+ }, done);
});
- it("should add a user by email", function (done) {
- assertScreenshotEquals("adminuser_give_view_access_via_email", done, function (page) {
- page.load(url);
- openGiveAccessForm(page);
- setLoginOrEmailForGiveAccessForm(page, 'login4@example.com');
- submitGiveAccessForm(page);
- });
+ it('should change access for all rows in search when confirmed', function (done) {
+ expect.screenshot("bulk_set_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.change-user-role-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
});
- it("should ask for confirmation when all sites selected", function (done) {
- assertScreenshotEquals("adminuser_all_users_loaded", done, function (page) {
- page.load(url + '&idSite=all');
- });
+ it('should remove access to the currently selected site when the bulk remove access option is clicked', function (done) {
+ expect.screenshot("bulk_remove_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('th.select-cell label'); // select displayed rows
+
+ page.click('.bulk-actions.btn');
+ page.click('#user-list-bulk-actions a:contains(Remove Permissions)');
+ page.click('.change-user-role-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should go back to first page when previous button is clicked', function (done) {
+ expect.screenshot("previous").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.usersListPagination .btn.prev');
+ }, done);
+ });
+
+ it('should delete a single user when the modal is confirmed is clicked', function (done) {
+ expect.screenshot("delete_single").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.deleteuser:eq(0)');
+ page.click('.delete-user-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should delete selected users when delete users bulk action is used', function (done) {
+ expect.screenshot("delete_bulk_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('th.select-cell label'); // select displayed rows
+
+ page.click('.bulk-actions.btn');
+ page.click('#user-list-bulk-actions a:contains(Delete Users)');
+ page.click('.delete-user-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should show the add new user form when the add new user button is clicked', function (done) {
+ expect.screenshot("add_new_user_form").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.add-user-container .btn');
+ }, done);
+ });
+
+ it('should create a user and show the edit user form when the create user button is clicked', function (done) {
+ expect.screenshot("user_created").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.sendKeys('#user_login', '000newuser');
+ page.sendKeys('#user_password', 'thepassword');
+ page.sendKeys('#user_email', 'theuser@email.com');
+
+ page.click('piwik-user-edit-form .siteSelector a.title');
+ page.click('piwik-user-edit-form .siteSelector .custom_select_container a:eq(1)');
+
+ page.click('piwik-user-edit-form [piwik-save-button]');
+ }, done);
+ });
+
+ it('should show the permissions edit when the permissions tab is clicked', function (done) {
+ expect.screenshot("permissions_edit").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userEditForm .menuPermissions');
+ page.sendMouseEvent('mousemove', { x: 0, y: 0 });
+ }, done);
+ });
+
+ it('should select all sites in search when in table link is clicked', function (done) {
+ expect.screenshot("permissions_all_rows_in_search").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ // remove filters
+ page.evaluate(function () {
+ $('div.site-filter>input').val('').change();
+ $('.access-filter select').val('string:').change();
+ });
+
+ page.click('.userPermissionsEdit th.select-cell label');
+ page.click('.userPermissionsEdit tr.select-all-row a');
+ }, done);
+ });
+
+ it('should add access to all websites when bulk access is used on all websites in search', function (done) {
+ expect.screenshot("permissions_all_sites_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userPermissionsEdit .bulk-actions > .dropdown-trigger.btn');
+ page.mouseMove('#user-permissions-edit-bulk-actions>li:first');
+ page.click('#user-permissions-edit-bulk-actions a:contains(Write)');
+
+ page.click('.change-access-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should go to the next results page when the next button is clicked', function (done) {
+ expect.screenshot("permissions_next").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.sites-for-permission-pagination a.next');
+ }, done);
+ });
+
+ it('should remove access to a single site when the trash icon is used', function (done) {
+ expect.screenshot("permissions_remove_single").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('#sitesForPermission .deleteaccess');
+ page.click('.delete-access-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should select multiple rows when individual row selects are clicked', function (done) {
+ expect.screenshot("permissions_select_multiple").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('#sitesForPermission td.select-cell label:eq(0)');
+ page.click('#sitesForPermission td.select-cell label:eq(3)');
+ page.click('#sitesForPermission td.select-cell label:eq(8)');
+ }, done);
+ });
+
+ it('should set access to selected sites when set bulk access is used', function (done) {
+ expect.screenshot("permissions_bulk_access_set").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userPermissionsEdit .bulk-actions > .dropdown-trigger.btn');
+ page.mouseMove('#user-permissions-edit-bulk-actions>li:first');
+ page.click('#user-permissions-edit-bulk-actions a:contains(Admin)');
+
+ page.click('.change-access-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should filter the permissions when the filters are used', function (done) {
+ expect.screenshot("permissions_filters").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.sendKeys('div.site-filter>input', 'nova');
+ page.evaluate(function () {
+ $('.access-filter select').val('string:admin').change();
+ });
+ }, done);
});
- it("should ask for confirmation when all sites selected", function (done) {
- expect.screenshot("adminuser_all_users_confirmation").to.be.captureSelector('.modal.open', function (page) {
- openGiveAccessForm(page);
- setLoginOrEmailForGiveAccessForm(page, 'login5@example.com');
- submitGiveAccessForm(page);
+ it('should select all displayed rows when the select all checkbox is clicked', function (done) {
+ expect.screenshot("permissions_select_all").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userPermissionsEdit th.select-cell label');
}, done);
});
+
+ it('should set access to all sites selected when set bulk access is used', function (done) {
+ expect.screenshot("permissions_bulk_access_set_all").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userPermissionsEdit .bulk-actions > .dropdown-trigger.btn');
+ page.mouseMove('#user-permissions-edit-bulk-actions>li:first');
+ page.click('#user-permissions-edit-bulk-actions a:contains(View)');
+
+ page.click('.change-access-confirm-modal .modal-close:not(.modal-no)');
+
+ page.evaluate(function () { // remove filter
+ $('.access-filter select').val('string:some').change();
+ });
+ }, done);
+ });
+
+ it('should set access to single site when select in table is used', function (done) {
+ expect.screenshot("permissions_single_site_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.evaluate(function () {
+ $('.userPermissionsEdit tr select:eq(0)').val('string:admin').change();
+ });
+
+ page.click('.change-access-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should remove access to displayed rows when remove bulk access is clicked', function (done) {
+ expect.screenshot("permissions_remove_access").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ // remove filters
+ page.evaluate(function () {
+ $('div.site-filter>input').val('').change();
+ $('.access-filter select').val('string:').change();
+ });
+
+ page.click('.userPermissionsEdit th.select-cell label');
+ page.click('.userPermissionsEdit tr.select-all-row a');
+
+ page.click('.userPermissionsEdit .bulk-actions > .dropdown-trigger.btn');
+ page.click('.userPermissionsEdit a:contains(Remove Permissions)');
+
+ page.click('.delete-access-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should display the superuser access tab when the superuser tab is clicked', function (done) {
+ expect.screenshot("superuser_tab").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userEditForm .menuSuperuser');
+ page.sendMouseEvent('mousemove', { x: 0, y: 0 });
+ }, done);
+ });
+
+ it('should show superuser confirm modal when the superuser toggle is clicked', function (done) {
+ expect.screenshot("superuser_confirm").to.be.captureSelector('.superuser-confirm-modal', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userEditForm #superuser_access+label');
+ }, done);
+ });
+
+ it('should give the user superuser access when the superuser modal is confirmed', function (done) {
+ expect.screenshot("superuser_set").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.superuser-confirm-modal .modal-close:not(.modal-no)');
+ }, done);
+ });
+
+ it('should go back to the manage users page when the back link is clicked', function (done) {
+ expect.screenshot("manage_users_back").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userEditForm .entityCancelLink');
+
+ page.evaluate(function () { // remove filter so new user shows
+ $('#user-text-filter').val('').change();
+ });
+ }, done);
+ });
+
+ it('should show the edit user form when the edit icon in a row is clicked', function (done) {
+ expect.screenshot("edit_user_form").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('button.edituser:eq(0)');
+ }, done);
+ });
+
+ // admin user tests
+ describe('UsersManager_admin_view', function () {
+ before(function () {
+ var idSites = [];
+ for (var i = 1; i !== 46; ++i) {
+ idSites.push(i);
+ }
+
+ testEnvironment.idSitesAdminAccess = idSites;
+ testEnvironment.save();
+ });
+
+ after(function () {
+ delete testEnvironment.idSitesAdminAccess;
+ testEnvironment.save();
+ });
+
+ it('should hide columns & functionality if an admin user views the manage user page', function (done) {
+ expect.screenshot("admin_load").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.load(url);
+ }, done);
+ });
+
+ it('should show the add user form for admin users', function (done) {
+ expect.screenshot("admin_add_user").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.add-user-container .btn');
+ }, done);
+ });
+
+ it('should not allow editing basic info for admin users', function (done) {
+ expect.screenshot("edit_user_basic_info").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.evaluate(function () {
+ $('.userEditForm .entityCancelLink').click();
+ });
+ page.click('button.edituser:eq(0)');
+ }, done);
+ });
+
+ it('should allow editing user permissions for admin users', function (done) {
+ expect.screenshot("admin_edit_permissions").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.userEditForm .menuPermissions');
+ }, done);
+ });
+
+ it('should show the add existing user modal', function (done) {
+ expect.screenshot("admin_existing_user_modal").to.be.captureSelector('.add-existing-user-modal', function (page) {
+ page.setViewportSize(1250);
+
+ page.evaluate(function () {
+ $('.userEditForm .entityCancelLink').click();
+ });
+
+ page.click('.add-existing-user');
+ }, done);
+ });
+
+ it('should add a user by email when an email is entered', function (done) {
+ expect.screenshot("admin_add_user_by_email").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.sendKeys('input[name=add-existing-user-email]', '0_login3conchords@example.com');
+ page.click('.add-existing-user-modal .modal-close:not(.modal-no)');
+
+ page.evaluate(function () { // show new user
+ $('#user-text-filter').val('0_login3conchords@example.com').change();
+ });
+ }, done);
+ });
+
+ it('should add a user by username when a username is entered', function (done) {
+ expect.screenshot("admin_add_user_by_login").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.add-existing-user');
+ page.sendKeys('input[name=add-existing-user-email]', '10_login8');
+ page.click('.add-existing-user-modal .modal-close:not(.modal-no)');
+
+ page.evaluate(function () { // show new user
+ $('#user-text-filter').val('10_login8').change();
+ });
+ }, done);
+ });
+
+ it('should fail if an email/username that does not exist is entered', function (done) {
+ expect.screenshot("admin_add_user_not_exists").to.be.captureSelector('.admin#content', function (page) {
+ page.setViewportSize(1250);
+
+ page.click('.add-existing-user');
+ page.sendKeys('input[name=add-existing-user-email]', 'sldkjfsdlkfjsdkl');
+ page.click('.add-existing-user-modal .modal-close:not(.modal-no)');
+
+ page.evaluate(function () { // show no user added
+ $('#user-text-filter').val('sldkjfsdlkfjsdkl').change();
+ });
+ }, done);
+ });
+ });
}); \ No newline at end of file
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_add_new_user_form.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_add_new_user_form.png
new file mode 100644
index 0000000000..fbf3ab242d
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_add_new_user_form.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99a06e52319f3f1cbb3b8e4645a894b01e629a3d57cf5dc1fb9507bfb661e3a7
+size 39758
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user.png
new file mode 100644
index 0000000000..fbf3ab242d
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99a06e52319f3f1cbb3b8e4645a894b01e629a3d57cf5dc1fb9507bfb661e3a7
+size 39758
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_email.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_email.png
new file mode 100644
index 0000000000..a20a6fc7e2
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_email.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fd1032b6338b98e506b315d3870d9fa69e7617bf4fc20046b0d784851a5fa1af
+size 32556
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_login.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_login.png
new file mode 100644
index 0000000000..db3dc2a710
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_by_login.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb1ed429d566b28811aaf9ab774875ab8d94ac5b5f167ee35ea72fd59d105bc1
+size 32579
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_not_exists.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_not_exists.png
new file mode 100644
index 0000000000..5f6f0b1510
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_add_user_not_exists.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:10b6270e84cb6aad331c4abd8e474829180b9a39d9f1bfa84268327c1aeba97f
+size 28966
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_edit_permissions.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_edit_permissions.png
new file mode 100644
index 0000000000..01c3f29b21
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_edit_permissions.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d76aabf1184c4a2df2e6db6fd0dd0db65b6c4099a1c54abc2af413d3d58dc65e
+size 19576
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_existing_user_modal.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_existing_user_modal.png
new file mode 100644
index 0000000000..3754be13e7
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_existing_user_modal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9b64c308416e3fa19b5add6434a86532c8b946d975e61c69e453a9261531c8c3
+size 10502
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_load.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_load.png
new file mode 100644
index 0000000000..a3d494ee66
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_admin_load.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1c58f35cb0ad50d6f5efae9cfc8fe33aec6264a1cefc6aaf32bb479231157f14
+size 110140
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_edit_user_basic_info.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_edit_user_basic_info.png
new file mode 100644
index 0000000000..6ef109b087
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_admin_view_edit_user_basic_info.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e2b72e7012d9840896a479fd0013f9ed6804c4545cc7f000714eebc95b3e296
+size 13062
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_confirmation.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_confirmation.png
deleted file mode 100644
index a97a8b434e..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_confirmation.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ec2171217c0069a4c9d47e665974ea6ca713fb877fc76adc0738324cc050ae55
-size 10601
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_loaded.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_loaded.png
deleted file mode 100644
index 5ec6df9cef..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_all_users_loaded.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e660cd6b1bb5e0bf50f0aca4a28831b815533a27341ab99b86970e16f1764355
-size 42243
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_form_opened.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_form_opened.png
deleted file mode 100644
index c370b52203..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_form_opened.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1fdc5d9b76a3085811d7fbbccb42f24592dfd11c0e9b7a19e8040d1f937106fc
-size 39943
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_no_user_entered.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_no_user_entered.png
deleted file mode 100644
index 13ff548721..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_no_user_entered.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:703dc27015f7faf13db1498e0783eed55ed6d5b77851b0da818b094ce9358953
-size 46249
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_already_has_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_already_has_access.png
deleted file mode 100644
index c0204ee9aa..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_already_has_access.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9a00cdcfd15f3d5847002ad30fd1e0625ae256d1fbce434b35251757583ade46
-size 46924
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_not_found.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_not_found.png
deleted file mode 100644
index b83c91c2ed..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_user_not_found.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:7474eb5cc4886b2be5010ea91768370aa3be04b2fcd75569df0da9b27968886d
-size 48719
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_email.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_email.png
deleted file mode 100644
index 3b854e5a7f..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_email.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d9cf30b1a30dc51994e2609144c1d1d64e6aa8ec856e9299383609eed4840c5f
-size 42097
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_login.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_login.png
deleted file mode 100644
index 351528b6fa..0000000000
--- a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_adminuser_give_view_access_via_login.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b31b8538a66d44eca57f52bc0d9cb92ddea34115baecd0f37cbb89847097ef54
-size 39387
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search.png
new file mode 100644
index 0000000000..30b50806ba
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9db249cf16b696e086679afcec4972d6033d59fba605241d5a0935e53cd11390
+size 161472
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search_deselected.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search_deselected.png
new file mode 100644
index 0000000000..6df3e1d38c
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_in_search_deselected.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a9ce5aa60a79e034647423ab64612edd3e40a261da43255dfa0cddb73c69c32
+size 161444
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_selected.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_selected.png
new file mode 100644
index 0000000000..6df3e1d38c
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_all_rows_selected.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a9ce5aa60a79e034647423ab64612edd3e40a261da43255dfa0cddb73c69c32
+size 161444
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_remove_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_remove_access.png
new file mode 100644
index 0000000000..fcfe5ccfa4
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_remove_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0ca0f4baac992335766aca363a20238d5b27aa3546d6ce8bde01f399be91ee6
+size 155864
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access.png
new file mode 100644
index 0000000000..8147e5ad21
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9979cef2b371921a599c168a655901cb0414ceea5f1dd2486459c9e437f73908
+size 141285
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access_confirm.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access_confirm.png
new file mode 100644
index 0000000000..5e251d38c8
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_bulk_set_access_confirm.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8871141e4aec73107fa4e9927ebeef81e51d57b059889a5b008d663fc44d9dfb
+size 9356
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_bulk_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_bulk_access.png
new file mode 100644
index 0000000000..a0fd4b0ba3
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_bulk_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d08e2a0c174afde9b4e46c6aaeae1fa4a4e9d5e181e78831e35c1d08b8eb61d
+size 154095
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_single.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_single.png
new file mode 100644
index 0000000000..310257e21e
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_delete_single.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa79bc9f5882f6678b5f47aaeaad52a02346943f0d224710dcd5d05f6db033d7
+size 141001
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_edit_user_form.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_edit_user_form.png
new file mode 100644
index 0000000000..a245168595
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_edit_user_form.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d1eb933aa76e0ca77d170af4ba676b80df828b8f09b69b9f54a34a143507ce6f
+size 23474
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_filters.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_filters.png
new file mode 100644
index 0000000000..1d0f7b59b1
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ec928e43b5a6637337d8b881615c7bbe85f5bc32880364565256d165fc6e83b
+size 139376
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_load.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_load.png
new file mode 100644
index 0000000000..b0f2fb8b25
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_load.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d38dc47f1064aca7e4f9bc3b3033d6aa3d53ac0aadcd492d95282acc7138be48
+size 150477
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_manage_users_back.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_manage_users_back.png
new file mode 100644
index 0000000000..784fd2b7d5
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_manage_users_back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e60f970adda0a4b8a15874a3472cad14cc4bb12939eca8811447e4dabbf3755
+size 148335
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_next_click.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_next_click.png
new file mode 100644
index 0000000000..f5d0de6593
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_next_click.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf80562d16eb62e702febb04ba3ec4285ca5e7f51c276782f2247c382ffbd6d1
+size 153037
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_rows_in_search.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_rows_in_search.png
new file mode 100644
index 0000000000..0022fd91b1
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_rows_in_search.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d36a9bf225eca03ee2bcf5d36a9d7baf6e85e16f7aabe36990fdcdde38c1c9f5
+size 79713
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_sites_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_sites_access.png
new file mode 100644
index 0000000000..2e429bbee3
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_all_sites_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fc4e7fb95394f4a4bdae4a1ff50c0b77bf8ad5ac157e30f6db6e5ffc524168b1
+size 60012
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set.png
new file mode 100644
index 0000000000..fa3cc7df5f
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9ece0d251ed5b08a68cfbed186a1d66a3dc8c91b72b76ae01c32a1def7b2ff6d
+size 47052
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set_all.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set_all.png
new file mode 100644
index 0000000000..19cb44a685
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_bulk_access_set_all.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b625878ad54adca6b44cce7322525ca8ec7ce840aa4b9a5efa91db3a3606585
+size 43368
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_edit.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_edit.png
new file mode 100644
index 0000000000..bd9bff21d6
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_edit.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2658dca747f807325db5fe6f3ff785bc2848d9708494254b6edd15f58548255
+size 22575
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_filters.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_filters.png
new file mode 100644
index 0000000000..f8c515a910
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8f45c225ebbfca893fe328d21c68fdddc8f282bbd921c65f7080c52ec5de343b
+size 23957
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_next.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_next.png
new file mode 100644
index 0000000000..41292a2b2e
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_next.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be94877ee54ce56686baf3a3090216cefd70cb3db6fbb26255fe71a142b71886
+size 48079
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_access.png
new file mode 100644
index 0000000000..e247d31e45
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4955560fa21665a8d3bbec183119e8970c2f82716e9dfae4bf65775117152276
+size 85229
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_single.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_single.png
new file mode 100644
index 0000000000..fecca2358c
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_remove_single.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92f833d59d60b0a7a558a77ce6820d6d2ddc66acde5cb5d2cec71eee8ed16d2e
+size 47172
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_all.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_all.png
new file mode 100644
index 0000000000..adcbff8b57
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_all.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7dacdfc90fe79d72aa5bc97b53120d772579da2b69bce8797fb2570352e84e4f
+size 26874
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_multiple.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_multiple.png
new file mode 100644
index 0000000000..308111dde5
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_select_multiple.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5df8344860559a450c7f1fcabbda73d2a41912ef86c6357dcaa3bced47a24a3
+size 50200
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_single_site_access.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_single_site_access.png
new file mode 100644
index 0000000000..f0790f3f4b
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_permissions_single_site_access.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5445b63d0108276898c9d101243aaa62de3e0062229d532408e6102fbe5968e
+size 43335
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_previous.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_previous.png
new file mode 100644
index 0000000000..86d1958e2d
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_previous.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0276c1e20a8d3c498d2e160adcfa807baed7f698b3956e8938cd6c312384985e
+size 140461
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_role_for.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_role_for.png
new file mode 100644
index 0000000000..33216df3fb
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_role_for.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:50229a0c2653a751dd7593dbc7e38fe838241f4b9080699108f97bb0187df8cb
+size 147447
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_rows_selected.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_rows_selected.png
new file mode 100644
index 0000000000..a86b8f8d66
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_rows_selected.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8654a005fd0e20251890b3de402533cb8960fb282d6efdaba5dcae85cdbe76b
+size 148735
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_confirm.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_confirm.png
new file mode 100644
index 0000000000..c733ad606a
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_confirm.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:34460d5f6f5e94595bdca89b3212464c8f8ae0d75f612526c55050e7fbf139c6
+size 16416
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_set.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_set.png
new file mode 100644
index 0000000000..014a891ed7
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_set.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98520d14766a5134c28bd026ff38d45266f331fdda90da73856e64b7fb347494
+size 37974
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_tab.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_tab.png
new file mode 100644
index 0000000000..2cfa6af082
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_superuser_tab.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d206dcc829bba02cf4329da750d30de301996b6b93fb66797257de8ce1561968
+size 37964
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_user_created.png b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_user_created.png
new file mode 100644
index 0000000000..4c6226b140
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UsersManager_user_created.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87fcc2c2f2bb1da7f2b3bff112ac3f8cf979b069124621ceab06de5b49318b4f
+size 23672
diff --git a/tests/PHPUnit/Integration/AccessTest.php b/tests/PHPUnit/Integration/AccessTest.php
index 755d075b0e..dcb1aca0cf 100644
--- a/tests/PHPUnit/Integration/AccessTest.php
+++ b/tests/PHPUnit/Integration/AccessTest.php
@@ -13,6 +13,8 @@ use Piwik\Access;
use Piwik\AuthResult;
use Piwik\Db;
use Piwik\NoAccessException;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
/**
@@ -524,6 +526,73 @@ class AccessTest extends IntegrationTestCase
});
}
+ public function test_getAccessForSite_whenUserHasAdminAccess()
+ {
+ $idSite = Fixture::createWebsite('2010-01-02 00:00:00');
+ UsersManagerAPI::getInstance()->addUser('testuser', 'testpass', 'testuser@email.com');
+ UsersManagerAPI::getInstance()->setUserAccess('testuser', 'admin', $idSite);
+
+ $this->switchUser('testuser');
+
+ Access::getInstance()->setSuperUserAccess(false);
+ $this->assertEquals('admin', Access::getInstance()->getRoleForSite($idSite));
+ }
+
+ public function test_getAccessForSite_whenUserHasViewAccess()
+ {
+ $idSite = Fixture::createWebsite('2010-01-03 00:00:00');
+ UsersManagerAPI::getInstance()->addUser('testuser', 'testpass', 'testuser@email.com');
+ UsersManagerAPI::getInstance()->setUserAccess('testuser', 'view', $idSite);
+
+ $this->switchUser('testuser');
+
+ Access::getInstance()->setSuperUserAccess(false);
+ $this->assertEquals('view', Access::getInstance()->getRoleForSite($idSite));
+ }
+
+ public function test_getAccessForSite_whenUserHasWriteAccess()
+ {
+ $idSite = Fixture::createWebsite('2010-01-03 00:00:00');
+ UsersManagerAPI::getInstance()->addUser('testuser', 'testpass', 'testuser@email.com');
+ UsersManagerAPI::getInstance()->setUserAccess('testuser', 'write', $idSite);
+
+ $this->switchUser('testuser');
+
+ Access::getInstance()->setSuperUserAccess(false);
+ $this->assertEquals('write', Access::getInstance()->getRoleForSite($idSite));
+ }
+
+ public function test_getAccessForSite_whenUserHasNoAccess()
+ {
+ $idSite = Fixture::createWebsite('2010-01-03 00:00:00');
+ UsersManagerAPI::getInstance()->addUser('testuser', 'testpass', 'testuser@email.com');
+
+ $this->switchUser('testuser');
+
+ Access::getInstance()->setSuperUserAccess(false);
+ $this->assertEquals('noaccess', Access::getInstance()->getRoleForSite($idSite));
+ }
+
+ public function test_getAccessForSite_whenUserIsSuperUser()
+ {
+ $idSite = Fixture::createWebsite('2010-01-03 00:00:00');
+
+ Access::getInstance()->setSuperUserAccess(true);
+ $this->assertEquals('admin', Access::getInstance()->getRoleForSite($idSite));
+ }
+
+ private function switchUser($user)
+ {
+ $mock = $this->createPiwikAuthMockInstance();
+ $mock->expects($this->once())
+ ->method('authenticate')
+ ->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, $user, 'token')));
+
+ Access::getInstance()->setSuperUserAccess(false);
+ Access::getInstance()->reloadAccess($mock);
+ Access::getInstance()->setSuperUserAccess(true);
+ }
+
private function buildAdminAccessForSiteIds($siteIds)
{
$access = array();
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_manage_users.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_manage_users.png
deleted file mode 100644
index f45de1af9d..0000000000
--- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_manage_users.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:91f49fa3a1c6f7ae3dba489f76bdf76b7b84c650de3a5d6050707af5bc2dab46
-size 108793
diff --git a/tests/UI/specs/UIIntegration_spec.js b/tests/UI/specs/UIIntegration_spec.js
index 2100dfad4a..1a2101554a 100644
--- a/tests/UI/specs/UIIntegration_spec.js
+++ b/tests/UI/specs/UIIntegration_spec.js
@@ -482,22 +482,6 @@ describe("UIIntegrationTest", function () { // TODO: Rename to Piwik?
}, done);
});
- it('should load the Manage > Users admin page correctly', function (done) {
- expect.screenshot('admin_manage_users').to.be.captureSelector('.pageWrap', function (page) {
- page.load("?" + generalParams + "&module=UsersManager&action=index");
-
- // remove token auth which can be random
- page.evaluate(function () {
- $('td#token_auth').each(function () {
- $(this).text('');
- });
- $('td#last_seen').each(function () {
- $(this).text( '' )
- });
- });
- }, done);
- });
-
it('should load the user settings admin page correctly', function (done) {
expect.screenshot('admin_user_settings').to.be.captureSelector('.pageWrap', function (page) {
page.load("?" + generalParams + "&module=UsersManager&action=userSettings");