Browse Source

install plugin

Kevin 3 years ago
parent
commit
c471d6ad2b
100 changed files with 6763 additions and 7 deletions
  1. 2 0
      user/accounts/bachir.yaml
  2. 2 0
      user/accounts/maud.yaml
  3. 12 7
      user/accounts/ouidade.yaml
  4. 2 0
      user/accounts/sandrine.yaml
  5. 38 0
      user/config/groups.yaml
  6. 2 0
      user/plugins/admin-addon-user-manager/.gitattributes
  7. 314 0
      user/plugins/admin-addon-user-manager/CHANGELOG.md
  8. 21 0
      user/plugins/admin-addon-user-manager/LICENSE
  9. 39 0
      user/plugins/admin-addon-user-manager/README.md
  10. 157 0
      user/plugins/admin-addon-user-manager/admin-addon-user-manager.php
  11. 4 0
      user/plugins/admin-addon-user-manager/admin-addon-user-manager.yaml
  12. 7 0
      user/plugins/admin-addon-user-manager/admin/pages/group-manager.md
  13. 7 0
      user/plugins/admin-addon-user-manager/admin/pages/user-expert.md
  14. 7 0
      user/plugins/admin-addon-user-manager/admin/pages/user-manager.md
  15. 3 0
      user/plugins/admin-addon-user-manager/assets/groups/style.css
  16. 227 0
      user/plugins/admin-addon-user-manager/assets/users/style.css
  17. 58 0
      user/plugins/admin-addon-user-manager/blueprints.yaml
  18. 20 0
      user/plugins/admin-addon-user-manager/blueprints/user/aaum-account.yaml
  19. 16 0
      user/plugins/admin-addon-user-manager/blueprints/user/account-raw.yaml
  20. 43 0
      user/plugins/admin-addon-user-manager/blueprints/user/group.yaml
  21. 10 0
      user/plugins/admin-addon-user-manager/composer.json
  22. 570 0
      user/plugins/admin-addon-user-manager/composer.lock
  23. 67 0
      user/plugins/admin-addon-user-manager/docs/filter.md
  24. 34 0
      user/plugins/admin-addon-user-manager/languages/cs.yaml
  25. 34 0
      user/plugins/admin-addon-user-manager/languages/en.yaml
  26. 34 0
      user/plugins/admin-addon-user-manager/languages/es.yaml
  27. 34 0
      user/plugins/admin-addon-user-manager/languages/fr.yaml
  28. 34 0
      user/plugins/admin-addon-user-manager/languages/no.yaml
  29. 34 0
      user/plugins/admin-addon-user-manager/languages/pt.yaml
  30. 34 0
      user/plugins/admin-addon-user-manager/languages/ru.yaml
  31. 34 0
      user/plugins/admin-addon-user-manager/languages/sr.yaml
  32. 34 0
      user/plugins/admin-addon-user-manager/languages/uk.yaml
  33. 34 0
      user/plugins/admin-addon-user-manager/languages/zh.yaml
  34. 93 0
      user/plugins/admin-addon-user-manager/modals.yaml
  35. 108 0
      user/plugins/admin-addon-user-manager/src/Dot.php
  36. 164 0
      user/plugins/admin-addon-user-manager/src/Group.php
  37. 242 0
      user/plugins/admin-addon-user-manager/src/Groups/Manager.php
  38. 58 0
      user/plugins/admin-addon-user-manager/src/Manager.php
  39. 81 0
      user/plugins/admin-addon-user-manager/src/Pagination/ArrayPagination.php
  40. 17 0
      user/plugins/admin-addon-user-manager/src/Pagination/Pagination.php
  41. 100 0
      user/plugins/admin-addon-user-manager/src/Users/ExpertManager.php
  42. 432 0
      user/plugins/admin-addon-user-manager/src/Users/Manager.php
  43. 25 0
      user/plugins/admin-addon-user-manager/templates/forms/fields/button/button.html.twig
  44. 170 0
      user/plugins/admin-addon-user-manager/templates/group-manager.html.twig
  45. 28 0
      user/plugins/admin-addon-user-manager/templates/user-expert.html.twig
  46. 31 0
      user/plugins/admin-addon-user-manager/templates/user-manager-macros.html.twig
  47. 199 0
      user/plugins/admin-addon-user-manager/templates/user-manager.html.twig
  48. 7 0
      user/plugins/admin-addon-user-manager/vendor/autoload.php
  49. 445 0
      user/plugins/admin-addon-user-manager/vendor/composer/ClassLoader.php
  50. 21 0
      user/plugins/admin-addon-user-manager/vendor/composer/LICENSE
  51. 16 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_classmap.php
  52. 10 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_files.php
  53. 9 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_namespaces.php
  54. 19 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_psr4.php
  55. 70 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_real.php
  56. 97 0
      user/plugins/admin-addon-user-manager/vendor/composer/autoload_static.php
  57. 574 0
      user/plugins/admin-addon-user-manager/vendor/composer/installed.json
  58. 22 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/LICENSE
  59. 5 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/build-phar.sh
  60. 34 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/composer.json
  61. 5 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey
  62. 11 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc
  63. 32 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/lib/random.php
  64. 57 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/other/build_phar.php
  65. 9 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/psalm-autoload.php
  66. 19 0
      user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/psalm.xml
  67. 16 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/CHANGELOG.md
  68. 19 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/LICENSE.txt
  69. 9 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/README.md
  70. 25 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/composer.json
  71. 10 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheException.php
  72. 105 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheItemInterface.php
  73. 138 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheItemPoolInterface.php
  74. 13 0
      user/plugins/admin-addon-user-manager/vendor/psr/cache/src/InvalidArgumentException.php
  75. 3 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/.gitignore
  76. 21 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/LICENSE
  77. 5 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/README.md
  78. 27 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/composer.json
  79. 13 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/src/ContainerExceptionInterface.php
  80. 37 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/src/ContainerInterface.php
  81. 13 0
      user/plugins/admin-addon-user-manager/vendor/psr/container/src/NotFoundExceptionInterface.php
  82. 19 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/LICENSE
  83. 128 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/AbstractLogger.php
  84. 7 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/InvalidArgumentException.php
  85. 18 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LogLevel.php
  86. 18 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerAwareInterface.php
  87. 26 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerAwareTrait.php
  88. 125 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerInterface.php
  89. 142 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerTrait.php
  90. 30 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/NullLogger.php
  91. 18 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/DummyTest.php
  92. 138 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php
  93. 147 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/TestLogger.php
  94. 58 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/README.md
  95. 26 0
      user/plugins/admin-addon-user-manager/vendor/psr/log/composer.json
  96. 3 0
      user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/.gitignore
  97. 57 0
      user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CacheInterface.php
  98. 76 0
      user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CacheTrait.php
  99. 30 0
      user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CallbackInterface.php
  100. 65 0
      user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/ItemInterface.php

+ 2 - 0
user/accounts/bachir.yaml

@@ -7,3 +7,5 @@ access:
 fullname: 'Bachir Soussi-Chiadmi'
 title: null
 hashed_password: $2y$10$qRbIZIzXKP/FCo5bBTt48uscJgeDl0g2G26oIfr2WUHQeiyZs4UDy
+groups:
+  - Contributeur

+ 2 - 0
user/accounts/maud.yaml

@@ -17,3 +17,5 @@ _json:
   avatar: '[]'
   state: '"enabled"'
   hashed_password: '"$2y$10$1ol4B2jLfCtepiNBZ4\/iveJmFLJn7PSTfEzlHJFSCNP9KymqCdC.C"'
+groups:
+  - Contributeur

+ 12 - 7
user/accounts/ouidade.yaml

@@ -3,12 +3,17 @@ fullname: 'Ouidade Soussi-Chiadmi'
 title: admin
 state: enabled
 access:
-  admin:
-    super: 'true'
-    login: 'true'
-  site:
-    login: 'true'
-hashed_password: $2y$10$6cgrT0dyJlSQ6Zy.oVkNYOh5bW8rFt.qnzexwn8.ct7kwXDnloIei
+  site: {  }
+  admin: {  }
+  admin-addon-user-manager: {  }
+hashed_password: $2y$10$eG2fDlTOSCGtanmNoVVLKOpsvoUk34KkCj2T.pW.K/EfS8CZ17seO
 language: en
 twofa_enabled: false
-twofa_secret: 2JBHUQEOVLQ5UCVOX7RE6R6ZK6KHW5FV
+twofa_secret: I6XKRSPKREKHDZKUBFVJRWVKQQCBNNS6
+_json:
+  avatar: '[]'
+  state: '"enabled"'
+  hashed_password: '"$2y$10$eG2fDlTOSCGtanmNoVVLKOpsvoUk34KkCj2T.pW.K\/EfS8CZ17seO"'
+groups:
+  - Administrateur
+password: null

+ 2 - 0
user/accounts/sandrine.yaml

@@ -17,3 +17,5 @@ _json:
   avatar: '[]'
   state: '"enabled"'
   hashed_password: '"$2y$10$OCR3MkWPb41krOmdZFrJae0L0.iI4d79Rv5Z.pZ68py4gsz\/RWfK2"'
+groups:
+  - Contributeur

+ 38 - 0
user/config/groups.yaml

@@ -0,0 +1,38 @@
+Contributeur:
+  groupname: Contributeur
+  access:
+    site:
+      login: 'true'
+    admin:
+      super: 'false'
+      login: 'true'
+      cache: 'true'
+      configuration: 'false'
+      configuration_system: 'false'
+      configuration_site: 'false'
+      configuration_media: 'false'
+      configuration_info: 'false'
+      settings: 'false'
+      pages: 'true'
+      maintenance: 'false'
+      statistics: 'true'
+      plugins: 'false'
+      themes: 'false'
+      tools: 'false'
+      users: 'false'
+    admin-addon-user-manager:
+      users: 'false'
+      groups: 'false'
+      users_expert: 'false'
+Administrateur:
+  groupname: Administrateur
+  access:
+    site:
+      login: 'true'
+    admin:
+      login: 'true'
+      pages: 'true'
+      statistics: 'true'
+      users: 'true'
+    admin-addon-user-manager:
+      users: 'true'

+ 2 - 0
user/plugins/admin-addon-user-manager/.gitattributes

@@ -0,0 +1,2 @@
+vendor/* linguist-generated=true
+composer.lock linguist-generated=true

+ 314 - 0
user/plugins/admin-addon-user-manager/CHANGELOG.md

@@ -0,0 +1,314 @@
+# v2.3.0
+## 05/04/2020
+
+1. [](#new)
+    * Chinese translation (Thanks: https://github.com/dallaslu PR #68)
+
+2. [](#improved)
+    * Replace deprecated User::load (#66)
+    * Updated dependencies
+    * Better compatibility with v1.7
+
+# v2.2.1
+## 12/11/2019
+
+1. [](#bugfix)
+    * Fix YAML linting error (#63)
+
+# v2.2.0
+## 10/11/2019
+
+1. [](#new)
+    * Brazilian Portuguese translation (Thanks: https://github.com/diegomagikal PR #61)
+    * Added "Enabled" toggle to user editor
+
+2. [](#bugfix)
+    * Fixed avatar upload
+
+# v2.1.8
+## 28/05/2019
+
+1. [](#new)
+    * French translation (Thanks: https://github.com/Miaourt PR #58)
+
+2. [](#bugfix)
+    * Fixed a problem with saving groups (#59)
+
+# v2.1.7
+##  13/02/2019
+
+1. [](#new)
+    * Serbian translation (Thanks: https://github.com/tomaja-linuxo PR #47)
+    * Russian translation (Thanks: https://github.com/Lufog-git PR #50)
+    * Ukrainian translation (Thanks: https://github.com/Lufog-git PR #51)
+
+2. [](#improved)
+    * Fixed some non-translatable strings (Thanks: https://github.com/Lufog-git PR #52)
+
+# v2.1.6
+##  06/06/2018
+
+1. [](#bugfix)
+    * Fixed error when using 'Login As' feature with an user without admin permissions (#43)
+
+# v2.1.5
+##  09/04/2018
+
+1. [](#bugfix)
+    * Fixed error when rendering front-end (#40)
+
+# v2.1.4
+##  09/04/2018
+
+1. [](#improved)
+    * Moved 'site.login' permission to the front of permission list. (#36)
+
+# v2.1.3
+##  02/04/2018
+
+1. [](#improved)
+    * Validate user object on save
+
+2. [](#bugfix)
+    * Fixed unset user permissions being pushed into the access array with an empty string value. Causing inherited permissions to be overwritten. (#38)
+
+# v2.1.2
+##  29/03/2018
+
+1. [](#new)
+    * Norwegian translation (Thanks: https://github.com/achwell PR #37)
+
+# v2.1.1
+##  22/03/2018
+
+1. [](#improved)
+    * Added 'site.login' permission to the permission list. (#36)
+
+# v2.1.0
+##  14/03/2018
+
+1. [](#new)
+    * Czech translation (Thanks: https://github.com/07pepa Issue #29)
+    * Spanish translation (Thanks: https://github.com/filisko PR #31)
+
+2. [](#bugfix)
+    * Fixed user editor using wrong task when saving, which caused save error when you didn't have 'admin.super'. (#34)
+    * Added a temporary fix for user editor's permission area. The toggles moved below the permission's name at a specific width. (#22)
+    * Minor bugfixes
+
+# v2.0.3
+##  27/02/2018
+
+1. [](#improved)
+    * Added missing translations
+
+# v2.0.2
+##  27/01/2018
+
+1. [](#bugfix)
+    * Fixed wrong redirection after deleting an user (#28)
+    * Added missing translation for user delete confirmation
+
+# v2.0.1
+##  01/01/2018
+
+1. [](#bugfix)
+    * Fixed admin links not working when something is changed in the form (#27)
+
+# v2.0.0
+##  29/12/2017
+
+1. [](#new)
+    * 'Login As' button
+
+2. [](#bugfix)
+    * Fixed being redirected to the deleted user, now redirects to the user manager
+    * The delete button now shows up when editing the user
+    * Avatar upload now works
+
+# v1.9.1
+##  29/12/2017
+
+1. [](#bugfix)
+    * Fixed 'Memory leak when using non-ascii character (?) to create group' (#26)
+    * Fixed being redirected to the deleted group, now redirects to the group manager
+
+# v1.9.0
+##  02/12/2017
+
+1. [](#improved)
+    * Using custom blueprint for user editing (#23)
+    * Using custom request handler for saving user data (#23)
+
+# v1.8.1
+##  18/09/2017
+
+1. [](#improved)
+    * Added username validating (#21)
+
+# v1.8.0
+##  14/08/2017
+
+1. [](#new)
+    * Custom permissions (#18)
+    * User Expert editor (#19)
+
+# v1.7.1
+##  08/08/2017
+
+1. [](#new)
+    * Permissions input in the bulk modal now accepts new values too
+
+# v1.7.0
+##  08/08/2017
+
+1. [](#new)
+    * User permissions bulk actions
+
+# v1.6.1
+##  08/06/2017
+
+1. [](#bugfix)
+    * Fixed removing of user from group not working. (#16, #17 Moonlight63 <https://github.com/Moonlight63>)
+
+# v1.6.0
+##  07/31/2017
+
+1. [](#new)
+    * User bulk actions
+    * Group bulk actions
+
+# v1.5.4
+##  07/28/2017
+
+1. [](#bugfix)
+    * Fixed groups.yaml is not created when saving a group for the first time
+
+# v1.5.3
+##  07/28/2017
+
+1. [](#bugfix)
+    * Fixed group creating not working properly
+
+# v1.5.2
+##  07/28/2017
+
+1. [](#bugfix)
+    * Fixed an error which appeared when there are no groups.yaml
+
+# v1.5.1
+##  07/28/2017
+
+1. [](#bugfix)
+    * Better PHP compatibility
+
+# v1.5.0
+##  07/28/2017
+
+1. [](#new)
+    * Filter users
+    * Filter groups
+
+# v1.4.3
+##  07/27/2017
+
+1. [](#new)
+    * Users count are now shown at group manager
+    * Users now can be added and/or removed from the group you are currently editing
+
+# v1.4.2
+##  07/27/2017
+
+1. [](#improved)
+    * Pagination performance improvement
+
+2. [](#bugfix)
+    * Fixed group name is not shown when editing a group
+
+# v1.4.1
+##  07/27/2017
+
+1. [](#improved)
+    * Permissions support
+
+# v1.4.0
+##  07/27/2017
+
+1. [](#feature)
+    * Groups management!
+
+# v1.3.4
+##  07/24/2017
+
+1. [](#improved)
+    * Plugin is now compatible with Grav Admin Styles Plugin
+
+# v1.3.3
+##  07/20/2017
+
+1. [](#new)
+    * Users to be shown per page is now configurable
+    * Default list style is now configurable
+
+2. [](#improved)
+    * Refactored code a bit
+
+# v1.3.2
+##  07/20/2017
+
+1. [](#improved)
+    * Performance is improved when admin cache is disabled
+
+1. [](#bugfix)
+    * Fixed plugin not working when admin cache is disabled
+
+# v1.3.1
+##  07/19/2017
+
+1. [](#improved)
+    * Redirects to last URL after user delete
+    * Prevents cache refresh after user delete (performance improvement)
+
+2. [](#bugfix)
+    * Params are now validated
+
+# v1.3.0
+##  07/19/2017
+
+1. [](#improved)
+    * Pagination is now more user friendly
+    * Users are cached (better performance)
+
+# v1.2.0
+##  07/19/2017
+
+1. [](#feature)
+    * Added pagination
+    * Added list style
+
+# v1.1.2
+##  07/19/2017
+
+1. [](#improved)
+    * No more page jumping because of avatars loading
+
+# v1.1.1
+##  07/06/2017
+
+1. [](#bugfix)
+    * Plugin is now compatible with PHP 5.5
+
+# v1.1.0
+##  07/03/2017
+
+1. [](#new)
+    * Delete users from the UI
+
+2. [](#improved)
+    * Revamped UI
+
+# v1.0.0
+##  06/27/2017
+
+1. [](#new)
+    * ChangeLog started...

+ 21 - 0
user/plugins/admin-addon-user-manager/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Dávid Szabó
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 39 - 0
user/plugins/admin-addon-user-manager/README.md

@@ -0,0 +1,39 @@
+# Admin Addon User Manager Plugin
+
+The **Admin Addon User Manager** Plugin is for [Grav CMS](http://github.com/getgrav/grav). A simple admin panel extension which adds the option to manage users and groups.
+
+## Installation
+
+Installing the Admin Addon User Manager plugin can be done in one of two ways. The GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file.
+
+### GPM Installation (Preferred)
+
+The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's terminal (also called the command line).  From the root of your Grav install type:
+
+    bin/gpm install admin-addon-user-manager
+
+This will install the Admin Addon User Manager plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/admin-addon-user-manager`.
+
+### Manual Installation
+
+To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `admin-addon-user-manager`. You can find these files on [GitHub](https://github.com/d-vid-szab-/grav-plugin-admin-addon-user-manager) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras).
+
+You should now have all the plugin files under
+
+    /your/site/grav/user/plugins/admin-addon-user-manager
+
+> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Admin](https://github.com/getgrav/grav-plugin-admin) plugin to operate.
+
+## Configuration
+
+Before configuring this plugin, you should copy the `user/plugins/admin-addon-user-manager/admin-addon-user-manager.yaml` to `user/config/plugins/admin-addon-user-manager.yaml` and only edit that copy.
+
+Here is the default configuration and an explanation of available options:
+
+```yaml
+enabled: true
+```
+
+## Usage
+
+Install the plugin and you are good to go. 'User Manager' and 'Group Manager' will appear in the sidebar of the admin panel.

+ 157 - 0
user/plugins/admin-addon-user-manager/admin-addon-user-manager.php

@@ -0,0 +1,157 @@
+<?php
+namespace Grav\Plugin;
+
+use Grav\Common\Plugin;
+use RocketTheme\Toolbox\Event\Event;
+use \Grav\Common\Utils;
+use \Grav\Common\User\User;
+use Grav\Common\File\CompiledYamlFile;
+use AdminAddonUserManager\Users\Manager as UsersManager;
+use AdminAddonUserManager\Groups\Manager as GroupsManager;
+use AdminAddonUserManager\Users\ExpertManager as UsersExpertManager;
+
+class AdminAddonUserManagerPlugin extends Plugin {
+
+  /**
+   * Returns the plugin's configuration key
+   *
+   * @param String $key
+   * @return String
+   */
+  public function getPluginConfigKey($key = null) {
+    $pluginKey = 'plugins.' . $this->name;
+
+    return ($key !== null) ? $pluginKey . '.' . $key : $pluginKey;
+  }
+
+  public function getPluginConfigValue($key = null, $default = null) {
+    return $this->config->get($this->getPluginConfigKey($key), $default);
+  }
+
+  public function getConfigValue($key, $default = null) {
+    return $this->config->get($key, $default);
+  }
+
+  public function getPreviousUrl() {
+    return $this->grav['session']->{$this->name . '.previous_url'};
+  }
+
+  public function getModalsConfiguration() {
+    return CompiledYamlFile::instance(__DIR__ . DS . 'modals.yaml')->content();
+  }
+
+  public static function getSubscribedEvents() {
+    return [
+      'onPluginsInitialized' => ['onPluginsInitialized', 0],
+      'onAdminRegisterPermissions' => ['onAdminRegisterPermissions', 1000]
+    ];
+  }
+
+  public function onPluginsInitialized() {
+    if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
+      return;
+    }
+
+    $this->grav['locator']->addPath('blueprints', '', __DIR__ . DS . 'blueprints');
+
+    include __DIR__ . DS . 'vendor' . DS . 'autoload.php';
+
+    $this->managers[] = new UsersManager($this->grav, $this);
+    $this->managers[] = new GroupsManager($this->grav, $this);
+    $this->managers[] = new UsersExpertManager($this->grav, $this);
+
+    $this->enable([
+      'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', -10],
+      'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
+      'onAdminMenu' => ['onAdminMenu', 0],
+      'onAssetsInitialized' => ['onAssetsInitialized', 0],
+      'onAdminTaskExecute' => ['onAdminTaskExecute', 0],
+    ]);
+
+    $this->registerPermissions();
+  }
+
+  public function onAssetsInitialized() {
+    $assets = $this->grav['assets'];
+
+    foreach ($this->managers as $manager) {
+      $manager->initializeAssets($assets);
+    }
+  }
+
+  public function onAdminMenu() {
+    $twig = $this->grav['twig'];
+    $twig->plugins_hooked_nav = (isset($twig->plugins_hooked_nav)) ? $twig->plugins_hooked_nav : [];
+
+    foreach ($this->managers as $manager) {
+      $nav = $manager->getNav();
+      if ($nav) {
+        $twig->plugins_hooked_nav[$nav['label']] = $nav;
+      }
+    }
+  }
+
+  public function onAdminTwigTemplatePaths($e) {
+    $paths = $e['paths'];
+    $paths[] = __DIR__ . DS . 'templates';
+    $e['paths'] = $paths;
+  }
+
+  public function onTwigSiteVariables() {
+    $page = $this->grav['page'];
+    $twig = $this->grav['twig'];
+    $session = $this->grav['session'];
+    $uri = $this->grav['uri'];
+
+    foreach ($this->managers as $manager) {
+      if ($page->slug() === $manager->getLocation() && $this->grav['admin']->authorize(['admin.super', $manager->getRequiredPermission()])) {
+        $session->{$this->name . '.previous_url'} = $uri->route() . $uri->params();
+
+        $page = $this->grav['admin']->page(true);
+        $page->id('aaum-' . implode('/', $uri->paths()));
+
+        $twig->twig_vars['context'] = $page;
+
+        $vars = $manager->handleRequest();
+        $twig->twig_vars = array_merge($twig->twig_vars, $vars);
+
+        return true;
+      }
+    }
+  }
+
+  public function onAdminTaskExecute($e) {
+    foreach ($this->managers as $manager) {
+      if ($this->grav['admin']->authorize(['admin.super', $manager->getRequiredPermission()])) {
+        $result = $manager->handleTask($e);
+
+        if ($result) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  public function registerPermissions() {
+    foreach ($this->managers as $manager) {
+      $this->grav['admin']->addPermissions([$manager->getRequiredPermission() => 'boolean']);
+    }
+
+    // Custom permissions
+    $customPermissions = $this->getPluginConfigValue('custom_permissions', []);
+    foreach ($customPermissions as $permission) {
+      $this->grav['admin']->addPermissions([$permission => 'boolean']);
+    }
+  }
+
+  public function onAdminRegisterPermissions() {
+    if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
+      return;
+    }
+
+    $this->grav['admin']->addPermissions(['site.login' => 'boolean']);
+  }
+
+}

+ 4 - 0
user/plugins/admin-addon-user-manager/admin-addon-user-manager.yaml

@@ -0,0 +1,4 @@
+enabled: true
+default_list_style: list
+pagination:
+  per_page: 20

+ 7 - 0
user/plugins/admin-addon-user-manager/admin/pages/group-manager.md

@@ -0,0 +1,7 @@
+---
+title: Group Manager
+access:
+    admin.groups: true
+    admin.login: true
+    admin.super: true
+---

+ 7 - 0
user/plugins/admin-addon-user-manager/admin/pages/user-expert.md

@@ -0,0 +1,7 @@
+---
+title: User Expert
+access:
+    admin.users_expert: true
+    admin.login: true
+    admin.super: true
+---

+ 7 - 0
user/plugins/admin-addon-user-manager/admin/pages/user-manager.md

@@ -0,0 +1,7 @@
+---
+title: User Manager
+access:
+    admin.users: true
+    admin.login: true
+    admin.super: true
+---

+ 3 - 0
user/plugins/admin-addon-user-manager/assets/groups/style.css

@@ -0,0 +1,3 @@
+.admin-addon-user-manager--group.admin-addon-user-manager--list .user__username {
+  flex: 1 0 auto;
+}

+ 227 - 0
user/plugins/admin-addon-user-manager/assets/users/style.css

@@ -0,0 +1,227 @@
+#admin-main .admin-block .admin-addon-user-manager h2 {
+  text-align: center;
+  font-size: 2rem;
+}
+
+/**
+ * Style chooser
+ */
+.admin-addon-user-manager-style {
+  float: right;
+  overflow: hidden;
+  font-size: 1.5em;
+  margin: 0 16px;
+}
+
+.admin-addon-user-manager-style i {
+  padding-left: 8px;
+}
+
+.admin-addon-user-manager .cell--header {
+  font-weight: bold;
+}
+
+/**
+ * Pagination
+ */
+.admin-addon-user-manager-pagination {
+  margin: 16px auto;
+  padding: 0;
+  text-align: center;
+}
+
+.admin-addon-user-manager-pagination ul {
+  list-style: none;
+  margin: 0 auto;
+  overflow: hidden;
+  clear: both;
+  display: inline-block;
+  padding: 0;
+}
+
+.admin-addon-user-manager-pagination li {
+  padding: 4px 8px;
+  border: 1px solid rgba(0, 0, 0, .24);
+  float: left;
+}
+
+.admin-addon-user-manager-pagination li.current {
+  background: #0082ba;
+  color: white;
+}
+
+.admin-addon-user-manager-pagination li + li {
+  border-left: 0;
+}
+
+/**
+ * Grid style
+ */
+.admin-addon-user-manager--grid {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+
+.admin-addon-user-manager--grid .cell {
+  flex: 0 0 100%;
+}
+
+.admin-addon-user-manager--grid .user {
+  margin: 16px;
+  border-radius: 2px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
+}
+
+.admin-addon-user-manager--grid .user__avatar {
+  display: block;
+  width: 100%;
+  height: 0;
+  position: relative;
+  padding-top: 100%;
+  overflow: hidden;
+}
+
+.admin-addon-user-manager--grid .user__avatar img {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: auto;
+  max-width: 100%;
+  display: block;
+  border-radius: 2px 2px 0 0;
+  object-fit: cover;
+}
+
+.admin-addon-user-manager--grid .user__username {
+  padding: 8px;
+}
+
+.admin-addon-user-manager--grid .user__email {
+  padding: 0 8px 8px 8px;
+}
+
+.admin-addon-user-manager--grid .user__actions {
+  padding: 8px;
+  border-top: 1px solid rgba(0, 0, 0, .14);
+  overflow: hidden;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.admin-addon-user-manager--grid .user__actions > a {
+  text-transform: uppercase;
+}
+
+.admin-addon-user-manager--grid .user__actions > a + a {
+  padding-left: 8px;
+}
+
+#admin-main .admin-addon-user-manager--grid .user__actions > a.delete {
+  color: #f44336;
+}
+
+#admin-main .admin-addon-user-manager--grid .user__actions > a.delete:hover {
+  color: #f6685e;
+}
+
+@media only screen and (min-width: 480px) {
+  .admin-addon-user-manager--grid .cell {
+    flex: 0 0 50%;
+  }
+}
+
+@media only screen and (min-width: 768px) {
+  .admin-addon-user-manager--grid .cell {
+    flex: 0 0 33.333%;
+  }
+}
+
+@media only screen and (min-width: 1400px) {
+  .admin-addon-user-manager--grid .cell {
+    flex: 0 0 20%;
+  }
+}
+
+/**
+ * List style
+ */
+.admin-addon-user-manager--list {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+}
+
+.admin-addon-user-manager--list .cell + .cell {
+  border-top: 1px solid rgba(0, 0, 0, .24);
+  padding-top: 8px;
+  margin-top: 8px;
+}
+
+.admin-addon-user-manager--list .user {
+  display: flex;
+  flex-direction: row;
+}
+
+.admin-addon-user-manager--list .user > * {
+  padding: 0 8px;
+}
+
+.admin-addon-user-manager--list .user__checkbox {
+  flex: 0 0 auto;
+}
+
+.admin-addon-user-manager--list .user__username {
+  flex: 0 0 25%;
+}
+
+.admin-addon-user-manager--list .user__email {
+  flex: 1 0 auto;
+}
+
+.admin-addon-user-manager--list .user__actions {
+  flex: 0 0 20%;
+}
+
+/**
+ * Filter
+ */
+.admin-addon-user-manager-filter {
+  display: flex;
+}
+
+.admin-addon-user-manager-filter__input {
+  flex: 1 0 auto;
+}
+
+.admin-addon-user-manager-filter__input .form-data {
+  padding-right: 1rem;
+}
+
+.admin-addon-user-manager-filter__help {
+  flex: 0 0 auto;
+  padding-right: 2rem;
+}
+
+#admin-main .admin-block .admin-addon-user-manager-filter__help .button {
+  padding-top: .425rem;
+  padding-bottom: .425rem;
+}
+
+/**
+ * Bulk
+ */
+.admin-addon-user-manager-bulk-action {
+  margin-left: 16px;
+}
+
+.button.warning:hover {
+  background: red;
+}
+
+.remodal[data-remodal-id="modal-admin-addon-user-manager-bulk"] input[type="checkbox"] {
+  display: none;
+}

+ 58 - 0
user/plugins/admin-addon-user-manager/blueprints.yaml

@@ -0,0 +1,58 @@
+name: Admin Addon User Manager
+version: 2.3.0
+description: A simple admin panel extension which adds the option to manage users and groups
+icon: plug
+author:
+  name: Dávid Szabó
+  email: david.szabo97@gmail.com
+homepage: https://github.com/david-szabo97/grav-plugin-admin-addon-user-manager
+keywords: grav, plugin, admin, media
+bugs: https://github.com/david-szabo97/grav-plugin-admin-addon-user-manager/issues
+docs: https://github.com/david-szabo97/grav-plugin-admin-addon-user-manager/blob/master/README.md
+license: MIT
+
+dependencies:
+    - { name: grav, version: '>=1.0.0' }
+    - { name: admin, version: '>=1.0.0' }
+
+form:
+  validation: strict
+  fields:
+    enabled:
+      type: toggle
+      label: PLUGIN_ADMIN.PLUGIN_STATUS
+      highlight: 1
+      default: 0
+      options:
+        1: PLUGIN_ADMIN.ENABLED
+        0: PLUGIN_ADMIN.DISABLED
+      validate:
+        type: bool
+
+    default_list_style:
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.DEFAULT_LIST_STYLE
+      type: select
+      options:
+        grid: PLUGIN_ADMIN_ADDON_USER_MANAGER.GRID
+        list: PLUGIN_ADMIN_ADDON_USER_MANAGER.LIST
+
+    pagination.per_page:
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.USERS_PER_PAGE
+      type: select
+      options:
+        10: 10
+        20: 20
+        30: 30
+        40: 40
+        50: 50
+        60: 60
+        70: 70
+        80: 80
+        90: 90
+        100: 100
+
+    custom_permissions:
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.CUSTOM_PERMISSIONS
+      type: array
+      placeholder_value: PLUGIN_ADMIN_ADDON_USER_MANAGER.CUSTOM_PERMISSIONS_PLACEHOLDER
+      value_only: true

+ 20 - 0
user/plugins/admin-addon-user-manager/blueprints/user/aaum-account.yaml

@@ -0,0 +1,20 @@
+extends@:
+  type: user/account
+  context: blueprints://
+
+form:
+  fields:
+    security:
+      unset-security@: true
+    state:
+      ordering@: content
+      type: toggle
+      label: PLUGIN_ADMIN.ENABLED
+      classes: twofa-toggle
+      highlight: enabled
+      default: enabled
+      options:
+        enabled: PLUGIN_ADMIN.YES
+        disabled: PLUGIN_ADMIN.NO
+      validate:
+        type: string

+ 16 - 0
user/plugins/admin-addon-user-manager/blueprints/user/account-raw.yaml

@@ -0,0 +1,16 @@
+title: User
+form:
+  fields:
+    raw:
+      type: editor
+      label: Raw
+      autofocus: true
+      codemirror:
+        mode: 'yaml'
+        indentUnit: 4
+        autofocus: true
+        indentWithTabs: false
+        lineNumbers: true
+        styleActiveLine: true
+        gutters: ['CodeMirror-lint-markers']
+        lint: true

+ 43 - 0
user/plugins/admin-addon-user-manager/blueprints/user/group.yaml

@@ -0,0 +1,43 @@
+title: Group
+form:
+  validation: loose
+
+  fields:
+    groupname:
+      type: text
+      size: large
+      label: PLUGIN_ADMIN.NAME
+      readonly: true
+
+    readableName:
+      type: text
+      size: large
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.READABLE_NAME
+
+    description:
+      type: text
+      size: large
+      label: PLUGIN_ADMIN.DESCRIPTION
+
+    icon:
+      type: text
+      size: small
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.ICON
+
+    access:
+      type: permissions
+      label: PLUGIN_ADMIN.PERMISSIONS
+      ignore_empty: true
+      validate:
+        type: array
+
+    users:
+      type: select
+      multiple: true
+      size: large
+      label: PLUGIN_ADMIN_ADDON_USER_MANAGER.USERS
+      data-options@: '\AdminAddonUserManager\Users\Manager::userNames'
+      classes: fancy
+      help: PLUGIN_ADMIN_ADDON_USER_MANAGER.USERS_HELP
+      validate:
+          type: commalist

+ 10 - 0
user/plugins/admin-addon-user-manager/composer.json

@@ -0,0 +1,10 @@
+{
+    "autoload": {
+        "psr-4": {
+            "AdminAddonUserManager\\": "src/"
+        }
+    },
+    "require": {
+        "symfony/expression-language": "^3.3"
+    }
+}

+ 570 - 0
user/plugins/admin-addon-user-manager/composer.lock

@@ -0,0 +1,570 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "a47ed7fe21ccca95f49cff0e9e3b871e",
+    "packages": [
+        {
+            "name": "paragonie/random_compat",
+            "version": "v9.99.99",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/random_compat.git",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*|5.*",
+                "vimeo/psalm": "^1"
+            },
+            "suggest": {
+                "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+            },
+            "type": "library",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com"
+                }
+            ],
+            "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+            "keywords": [
+                "csprng",
+                "polyfill",
+                "pseudorandom",
+                "random"
+            ],
+            "time": "2018-07-02T15:55:56+00:00"
+        },
+        {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "time": "2016-08-06T20:24:11+00:00"
+        },
+        {
+            "name": "psr/container",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+                "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "time": "2017-02-14T16:28:37+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2020-03-23T09:12:05+00:00"
+        },
+        {
+            "name": "symfony/cache",
+            "version": "v4.4.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/cache.git",
+                "reference": "f777b570291aebe51081b9827e05f3a747665e87"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/cache/zipball/f777b570291aebe51081b9827e05f3a747665e87",
+                "reference": "f777b570291aebe51081b9827e05f3a747665e87",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "psr/cache": "~1.0",
+                "psr/log": "~1.0",
+                "symfony/cache-contracts": "^1.1.7|^2",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/var-exporter": "^4.2|^5.0"
+            },
+            "conflict": {
+                "doctrine/dbal": "<2.5",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/http-kernel": "<4.4",
+                "symfony/var-dumper": "<4.4"
+            },
+            "provide": {
+                "psr/cache-implementation": "1.0",
+                "psr/simple-cache-implementation": "1.0",
+                "symfony/cache-implementation": "1.0"
+            },
+            "require-dev": {
+                "cache/integration-tests": "dev-master",
+                "doctrine/cache": "~1.6",
+                "doctrine/dbal": "~2.5",
+                "predis/predis": "~1.1",
+                "psr/simple-cache": "^1.0",
+                "symfony/config": "^4.2|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.1|^5.0",
+                "symfony/var-dumper": "^4.4|^5.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Cache\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Cache component with PSR-6, PSR-16, and tags",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "caching",
+                "psr6"
+            ],
+            "time": "2020-03-27T16:54:36+00:00"
+        },
+        {
+            "name": "symfony/cache-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/cache-contracts.git",
+                "reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
+                "reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/cache": "^1.0"
+            },
+            "suggest": {
+                "symfony/cache-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Cache\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to caching",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
+        },
+        {
+            "name": "symfony/expression-language",
+            "version": "v3.4.39",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/expression-language.git",
+                "reference": "206165f46c660f3231df0afbdeec6a62f81afc59"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/expression-language/zipball/206165f46c660f3231df0afbdeec6a62f81afc59",
+                "reference": "206165f46c660f3231df0afbdeec6a62f81afc59",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/cache": "~3.1|~4.0",
+                "symfony/polyfill-php70": "~1.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\ExpressionLanguage\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony ExpressionLanguage Component",
+            "homepage": "https://symfony.com",
+            "time": "2020-03-16T08:31:04+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php70",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php70.git",
+                "reference": "2a18e37a489803559284416df58c71ccebe50bf0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0",
+                "reference": "2a18e37a489803559284416df58c71ccebe50bf0",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/random_compat": "~1.0|~2.0|~9.99",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php70\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
+        },
+        {
+            "name": "symfony/var-exporter",
+            "version": "v5.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/var-exporter.git",
+                "reference": "ffd29a70370e466343e33154b5df197a07a13afa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ffd29a70370e466343e33154b5df197a07a13afa",
+                "reference": "ffd29a70370e466343e33154b5df197a07a13afa",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "require-dev": {
+                "symfony/var-dumper": "^4.4|^5.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\VarExporter\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "clone",
+                "construct",
+                "export",
+                "hydrate",
+                "instantiate",
+                "serialize"
+            ],
+            "time": "2020-03-27T16:56:45+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}

+ 67 - 0
user/plugins/admin-addon-user-manager/docs/filter.md

@@ -0,0 +1,67 @@
+# Filter expression
+
+## Users
+### Available variables
+* `user.username` username of the user
+* `user.email` email of the user
+* `user.fullName` full name of the user
+* `user.title` title of the user
+* `user.language` language of the user
+* `user.groups` array of the groups the user is in
+* `user.access` an array which contains the permissions of the user
+
+### Available methods
+* `user.authorize('example.permission')` checks whether user has access to the given permission or not (take groups into account)
+
+### Examples
+* filter users by permissions
+  ```
+  user.authorize('admin.super')
+  ```
+* show users who are in the 'paid' group
+  ```
+  'paid' in user.groups
+  ```
+* show users without groups
+  ```
+  count(user.groups) > 0
+  ```
+* show users with access to 'admin.users'
+  ```
+  group.authorize('admin.users') and groups.users > 0
+* show users with gmail email provider
+  ```
+  user.email matches '/@gmail.com/'
+  ```
+
+## Groups
+### Available variables
+* `group.groupname` name of the group
+* `group.readableName` readable name of the group
+* `group.description` description of the group
+* `group.icon` icon of the group
+* `group.access` an array which contains the permissions of the group
+
+### Available methods
+* `group.authorize('example.permission')` checks whether group has access to the given permission or not
+
+### Examples
+* filter groups by permissions
+  ```
+  group.authorize('admin.super')
+  ```
+* show groups with more than 5 users
+  ```
+  group.users > 5
+  ```
+* show empty groups
+  ```
+  group.users == 0
+  ```
+* show groups with access to 'admin.users' and not empty
+  ```
+  group.authorize('admin.users') and groups.users > 0
+* show groups which contains 'admin' in its description
+  ```
+  group.description matches '/admin/'
+  ```

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/cs.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Správce uživatelů
+  GROUP_MANAGER: Správce skupin
+  FILTER_ERROR: Chyba filtru
+  FILTER_USERS: Filtruj uživatele
+  FILTER_GROUPS: Filtruj skupiny
+  NO_RESULTS: Žádné shody
+  EXPERT: Expert
+  PAGINATION_SUMMARY: Zobrazeno %d - %d z %d
+  BULK_ACTION: Hromadné akce
+  ACTIONS: Akce
+  CUSTOM_PERMISSIONS: Specifická opravnění
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "opravnění (např. 'site.login')"
+  USERS_PER_PAGE: Uživatelů na stránku
+  DEFAULT_LIST_STYLE: Výchozí styl listu
+  GRID: Mřížka
+  LIST: Stránka
+  GROUP: Skupina
+  GROUPS: Skupiny
+  READABLE_NAME: Zobrazené jméno
+  ICON: Ikona
+  ADD_GROUP: Přidat skupinu
+  USERS: Uživatelé
+  HELP: Pomoc
+  BULK_DELETE_USER: Delete selected users
+  BULK_DELETE: Delete
+  BULK_USER_GROUP: Bulk user actions related to groups
+  BULK_ADD_GROUP: Add selected users to selected groups
+  BULK_REMOVE_GROUP: Odeber vybrané uživatele
+  BULK_DELETE_GROUP: Odeber vybrané skupiny
+  BULK_ADD_PERMISSIONS: Přidej vybraná opravnění vybraným
+  BULK_REMOVE_PERMISSIONS: Odeber vybraná opravnění vybraným uživatelům
+  LOGIN_AS: Přihlaš se jako
+  USER_CONFIRM_DELETE: Opravdu chcete smazat tohoto uživatele?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/en.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Users Manager
+  GROUP_MANAGER: Groups Manager
+  FILTER_ERROR: Filter Error
+  FILTER_USERS: Filter users
+  FILTER_GROUPS: Filter groups
+  NO_RESULTS: No results
+  EXPERT: Expert
+  PAGINATION_SUMMARY: Showing %d - %d of %d
+  BULK_ACTION: Bulk Action
+  ACTIONS: Actions
+  CUSTOM_PERMISSIONS: Custom permissions
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Permission (e.g. 'site.login')"
+  USERS_PER_PAGE: Users per page
+  DEFAULT_LIST_STYLE: Default list style
+  GRID: Grid
+  LIST: List
+  GROUP: Group
+  GROUPS: Groups
+  READABLE_NAME: Readable name
+  ICON: Icon
+  ADD_GROUP: Add Group
+  USERS: Users
+  HELP: Help
+  BULK_DELETE_USER: Delete selected users
+  BULK_DELETE: Delete
+  BULK_USER_GROUP: Bulk user actions related to groups
+  BULK_ADD_GROUP: Add selected users to selected groups
+  BULK_REMOVE_GROUP: Remove selected users from selected groups
+  BULK_DELETE_GROUP: Delete selected groups
+  BULK_ADD_PERMISSIONS: Add selected permissions to selected users
+  BULK_REMOVE_PERMISSIONS: Remove selected permissions from selected users
+  LOGIN_AS: Login as
+  USER_CONFIRM_DELETE: Are you sure you want to delete this user?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/es.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Usuarios
+  GROUP_MANAGER: Grupos
+  FILTER_ERROR: Error de filtro
+  FILTER_USERS: Filtrar usuarios
+  FILTER_GROUPS: Filtrar grupos
+  NO_RESULTS: Sin resultados
+  EXPERT: Experto
+  PAGINATION_SUMMARY: Mostrando %d - %d de %d
+  BULK_ACTION: Acciones en masa
+  ACTIONS: Acciones
+  CUSTOM_PERMISSIONS: Permisos customizados
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Permiso (e.g. 'site.login')"
+  USERS_PER_PAGE: Usuarios por página
+  DEFAULT_LIST_STYLE: Estilo de lista predeterminado
+  GRID: Grid
+  LIST: Lista
+  GROUP: Grupo
+  GROUPS: Grupos
+  READABLE_NAME: Nombre legible
+  ICON: Icono
+  ADD_GROUP: Añadir Grupo
+  USERS: Usuarios
+  HELP: Ayuda
+  BULK_DELETE_USER: Eliminar usuarios seleccionados
+  BULK_DELETE: Eliminar
+  BULK_USER_GROUP: Acciones masivas de usuarios relacionadas con grupos
+  BULK_ADD_GROUP: Agregar usuarios seleccionados a los grupos seleccionados
+  BULK_REMOVE_GROUP: Eliminar usuarios seleccionados de los grupos seleccionados
+  BULK_DELETE_GROUP: Eliminar grupos seleccionados
+  BULK_ADD_PERMISSIONS: Añadir permisos seleccionados a los usuarios seleccionados
+  BULK_REMOVE_PERMISSIONS: Eliminar permisos seleccionados de los usuarios seleccionados
+  LOGIN_AS: Entrar como
+  USER_CONFIRM_DELETE: ¿Estás seguro de que deseas eliminar este usuario?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/fr.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Utilisateurs
+  GROUP_MANAGER: Groupes
+  FILTER_ERROR: Erreur de filtre
+  FILTER_USERS: Filtrer les utilisateurs
+  FILTER_GROUPS: Filtrer les groupes
+  NO_RESULTS: Pas de resultats
+  EXPERT: Expert
+  PAGINATION_SUMMARY: Afficher %d - %d de %d
+  BULK_ACTION: Action en bloc
+  ACTIONS: Actions
+  CUSTOM_PERMISSIONS: Permissions personnalisées
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Permission (par ex. 'site.login')"
+  USERS_PER_PAGE: Utilisateurs par page
+  DEFAULT_LIST_STYLE: Style de liste par défaut
+  GRID: Grille
+  LIST: Liste
+  GROUP: Groupe
+  GROUPS: Groupes
+  READABLE_NAME: Nom lisible
+  ICON: Icone
+  ADD_GROUP: Ajouter un groupe
+  USERS: Utilisateurs
+  HELP: Aide
+  BULK_DELETE_USER: Supprimer les utilisateurs sélectionnés
+  BULK_DELETE: Supprimer
+  BULK_USER_GROUP: Actions en bloc liées aux groupes
+  BULK_ADD_GROUP: Ajouter des utilisateurs sélectionnés aux groupes sélectionnés
+  BULK_REMOVE_GROUP: Supprimer les utilisateurs sélectionnés des groupes sélectionnés
+  BULK_DELETE_GROUP: Supprimer les groupes sélectionnés
+  BULK_ADD_PERMISSIONS: Ajouter les permissions sélectionnées aux utilisateurs sélectionnés
+  BULK_REMOVE_PERMISSIONS: Supprimer les permissions sélectionnées des utilisateurs sélectionnés
+  LOGIN_AS: Se connecter en tant que
+  USER_CONFIRM_DELETE: Êtes-vous sûr de vouloir supprimer cet utilisateur ?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/no.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Brukere
+  GROUP_MANAGER: Grupper
+  FILTER_ERROR: Ingen treff
+  FILTER_USERS: Filtrer brukere
+  FILTER_GROUPS: Filtrer grupper
+  NO_RESULTS: Ingen treff
+  EXPERT: Ekspert
+  PAGINATION_SUMMARY: Viser %d - %d av %d
+  BULK_ACTION: Bulk Handlinger
+  ACTIONS: Handlinger
+  CUSTOM_PERMISSIONS: Tilganger
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Tilganger (f.eks. 'site.login')"
+  USERS_PER_PAGE: Brukere pr side
+  DEFAULT_LIST_STYLE: Standard listevisning
+  GRID: Rutenett
+  LIST: Liste
+  GROUP: Gruppe
+  GROUPS: Grupper
+  READABLE_NAME: Lesbart navn
+  ICON: Ikon
+  ADD_GROUP: Legg til gruppe
+  USERS: Brukere
+  HELP: Hjelp
+  BULK_DELETE_USER: Slett valgte brukere
+  BULK_DELETE: Slett
+  BULK_USER_GROUP: Bulk brukerhandlinger relatert til grupper
+  BULK_ADD_GROUP: Legg valgte brukere til valgte grupper
+  BULK_REMOVE_GROUP: Fjern valgte brukere fra valgte grupper
+  BULK_DELETE_GROUP: Slett valgte grupper
+  BULK_ADD_PERMISSIONS: Legg valgte rettigheter til valgte brukere
+  BULK_REMOVE_PERMISSIONS: Fjern valgte rettigheter fra valgte brukere
+  LOGIN_AS: Logg inn som
+  USER_CONFIRM_DELETE: Er du sikker på at du vil slette denne brukeren?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/pt.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Gerenciar usuários
+  GROUP_MANAGER: Gerenciar grupos
+  FILTER_ERROR: Erro no filtro
+  FILTER_USERS: Filtrar usuários
+  FILTER_GROUPS: Filtrar grupos
+  NO_RESULTS: Nenhum resultado
+  EXPERT: Avançado
+  PAGINATION_SUMMARY: Mostrando %d - %d de %d
+  BULK_ACTION: Ações em massa
+  ACTIONS: Ações
+  CUSTOM_PERMISSIONS: Permissões personalizadas
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Permissão (ex. 'site.login')"
+  USERS_PER_PAGE: Usuários por página
+  DEFAULT_LIST_STYLE: Estilo padrão da listagem
+  GRID: Grade
+  LIST: Lista
+  GROUP: Grupo
+  GROUPS: Grupos
+  READABLE_NAME: Nome de leitura
+  ICON: Ícone
+  ADD_GROUP: Adicionar grupo
+  USERS: Usuários
+  HELP: Ajuda
+  BULK_DELETE_USER: Excluir usuários slecionados
+  BULK_DELETE: Excluir
+  BULK_USER_GROUP: Ações em massa do usuário relacionadas a grupos
+  BULK_ADD_GROUP: Adicionar usuários selecionados aos grupos selecionados
+  BULK_REMOVE_GROUP: Remover usuários selecionados dos grupos selecionados
+  BULK_DELETE_GROUP: Excluir grupos selecionados
+  BULK_ADD_PERMISSIONS: Adicionar permissões selecionadas aos usuários selecionados
+  BULK_REMOVE_PERMISSIONS: Remover permissões selecionadas dos usuários selecionados
+  LOGIN_AS: Entrar como
+  USER_CONFIRM_DELETE: Você tem certeza que deseja excluir este usuário?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/ru.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Пользователи
+  GROUP_MANAGER: Группы пользователей
+  FILTER_ERROR: Ошибка фильтрации
+  FILTER_USERS: Фильтр пользователей
+  FILTER_GROUPS: Фильтр групп
+  NO_RESULTS: Нет результатов
+  EXPERT: Экспертный
+  PAGINATION_SUMMARY: Страница %d - %d из %d
+  BULK_ACTION: Групповые действия
+  ACTIONS: Действия
+  CUSTOM_PERMISSIONS: Пользовательские разрешения
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Разрешение (например, 'site.login')"
+  USERS_PER_PAGE: Пользователи на странице
+  DEFAULT_LIST_STYLE: Стиль списка по умолчанию
+  GRID: Сетка
+  LIST: Список
+  GROUP: Группа
+  GROUPS: Группы
+  READABLE_NAME: Читаемое имя
+  ICON: Иконка
+  ADD_GROUP: Добавить группу
+  USERS: Пользователи
+  HELP: Помощь
+  BULK_DELETE_USER: Удалить выбранных пользователей
+  BULK_DELETE: Удалить
+  BULK_USER_GROUP: Массовые пользовательские действия, связанные с группами
+  BULK_ADD_GROUP: Добавить выбранных пользователей в выбранные группы
+  BULK_REMOVE_GROUP: Удалить выбранных пользователей из выбранных групп
+  BULK_DELETE_GROUP: Удалить выбранные группы
+  BULK_ADD_PERMISSIONS: Добавить выбранные разрешения для выбранных пользователей
+  BULK_REMOVE_PERMISSIONS: Удалить выбранные разрешения у выбранных пользователей
+  LOGIN_AS: Войти как
+  USER_CONFIRM_DELETE: Вы уверены, что хотите удалить этого пользователя?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/sr.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Менаџер Корисника
+  GROUP_MANAGER: Менаџер Група
+  FILTER_ERROR: Грешка у филтрирању
+  FILTER_USERS: Филтрирај кориснике
+  FILTER_GROUPS: Филтрирај групе
+  NO_RESULTS: Нема резултата
+  EXPERT: Напредно
+  PAGINATION_SUMMARY: Приказујем %d - %d од %d
+  BULK_ACTION: Групна Акција
+  ACTIONS: Акције
+  CUSTOM_PERMISSIONS: Прилагођена овлашћења
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Овлашћења (e.g. 'site.login')"
+  USERS_PER_PAGE: Корисника по страници
+  DEFAULT_LIST_STYLE: Подразумевани изглед листе
+  GRID: Мрежа
+  LIST: Листа
+  GROUP: Група
+  GROUPS: Групе
+  READABLE_NAME: Читљиво име
+  ICON: Икона
+  ADD_GROUP: Додај групу
+  USERS: Корисници
+  HELP: Помоћ
+  BULK_DELETE_USER: Обриши изабране кориснике
+  BULK_DELETE: Обриши
+  BULK_USER_GROUP: Групна корисничка акција везана за фрупе
+  BULK_ADD_GROUP: Додај изабране кориснике у изабране групе
+  BULK_REMOVE_GROUP: Уклони изабране кориснике из изабраних група
+  BULK_DELETE_GROUP: Обриши изабране кориснике
+  BULK_ADD_PERMISSIONS: Додај изабрана овлашћења изабраним корисницима
+  BULK_REMOVE_PERMISSIONS: Уклони изабрана овлашћења изабраним корисницима
+  LOGIN_AS: Пријављује се као
+  USER_CONFIRM_DELETE: Да ли си сигуран да желиш да обришеш овог корисника?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/uk.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: Користувачі
+  GROUP_MANAGER: Групи користувачів
+  FILTER_ERROR: Помилка фільтрації
+  FILTER_USERS: Фільтр користувачів
+  FILTER_GROUPS: Фільтр груп
+  NO_RESULTS: Немає результатів
+  EXPERT: Експертний
+  PAGINATION_SUMMARY: Сторінка %d - %d з %d
+  BULK_ACTION: Групові дії
+  ACTIONS: Дії
+  CUSTOM_PERMISSIONS: Призначені для користувача дозволи
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "Дозвіл (наприклад, 'site.login')"
+  USERS_PER_PAGE: Користувачі на сторінці
+  DEFAULT_LIST_STYLE: Стиль списку за замовчуванням
+  GRID: Сітка
+  LIST: Список
+  GROUP: Група
+  GROUPS: Групи
+  READABLE_NAME: Читаєме ім'я
+  ICON: Іконка
+  ADD_GROUP: Додати групу
+  USERS: Користувачі
+  HELP: Допомога
+  BULK_DELETE_USER: Видалити вибраних користувачів
+  BULK_DELETE: Видалити
+  BULK_USER_GROUP: Масові дії користувача, пов'язані з групами
+  BULK_ADD_GROUP: Додати обраних користувачів в обрані групи
+  BULK_REMOVE_GROUP: Видалити вибраних користувачів з обраних груп
+  BULK_DELETE_GROUP: Видалити вибрані групи
+  BULK_ADD_PERMISSIONS: Додати вибрані дозволи для обраних користувачів
+  BULK_REMOVE_PERMISSIONS: Видалити вибрані дозволи у вибраних користувачів
+  LOGIN_AS: Увійти як
+  USER_CONFIRM_DELETE: Ви впевнені, що хочете видалити цього користувача?

+ 34 - 0
user/plugins/admin-addon-user-manager/languages/zh.yaml

@@ -0,0 +1,34 @@
+PLUGIN_ADMIN_ADDON_USER_MANAGER:
+  USER_MANAGER: 用户管理
+  GROUP_MANAGER: 组管理
+  FILTER_ERROR: 筛选错误
+  FILTER_USERS: 筛选用户
+  FILTER_GROUPS: 筛选组
+  NO_RESULTS: 没有内容
+  EXPERT: 专家
+  PAGINATION_SUMMARY: 正在显示 %d - %d (共 %d)
+  BULK_ACTION: 批量操作
+  ACTIONS: 操作
+  CUSTOM_PERMISSIONS: 自定义权限
+  CUSTOM_PERMISSIONS_PLACEHOLDER: "权限 (例如 'site.login')"
+  USERS_PER_PAGE: 每页用户数
+  DEFAULT_LIST_STYLE: 默认列表风格
+  GRID: 网格
+  LIST: 列表
+  GROUP: 组
+  GROUPS: 组
+  READABLE_NAME: 显示名称
+  ICON: 图标
+  ADD_GROUP: 添加组
+  USERS: 用户
+  HELP: 帮助
+  BULK_DELETE_USER: 删除所选用户
+  BULK_DELETE: 删除
+  BULK_USER_GROUP: 批量用户与关联组操作
+  BULK_ADD_GROUP: 添加所选用户到指定组
+  BULK_REMOVE_GROUP: 从指定组中移除所选用户
+  BULK_DELETE_GROUP: 删除所选组
+  BULK_ADD_PERMISSIONS: 向所选用户添加指定权限
+  BULK_REMOVE_PERMISSIONS: 从所选用户上移除指定权限
+  LOGIN_AS: 模拟登录
+  USER_CONFIRM_DELETE: 确认要删除该用户吗?

+ 93 - 0
user/plugins/admin-addon-user-manager/modals.yaml

@@ -0,0 +1,93 @@
+add_user:
+  fields:
+  - type: section
+    title: PLUGIN_ADMIN.ADD_ACCOUNT
+
+  - type: text
+    label: PLUGIN_ADMIN.USERNAME
+    help: PLUGIN_ADMIN.USERNAME_HELP
+    name: username
+    placeholder: PLUGIN_ADMIN.USERNAME_PLACEHOLDER
+    validate:
+      required: true
+      message: PLUGIN_LOGIN.USERNAME_NOT_VALID
+
+add_group:
+  fields:
+  - type: section
+    title: PLUGIN_ADMIN_ADDON_USER_MANAGER.ADD_GROUP
+
+  - type: text
+    label: PLUGIN_ADMIN.NAME
+    name: name
+    validate:
+      required: true
+
+bulk_user:
+  fields:
+  - type: section
+    title: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_DELETE_USER
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_DELETE
+    name: bulk_delete
+    style: vertical
+    classes: large warning
+
+  - type: section
+    title: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_USER_GROUP
+
+  - type: select
+    name: groups
+    multiple: true
+    size: large
+    label: PLUGIN_ADMIN.GROUPS
+    classes: fancy
+    help: PLUGIN_ADMIN.GROUPS_HELP
+    validate:
+        type: commalist
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_ADD_GROUP
+    name: bulk_add_to_group
+    style: vertical
+    classes: large
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_REMOVE_GROUP
+    name: bulk_remove_from_group
+    style: vertical
+    classes: large warning
+
+  - type: selectize
+    name: permissions
+    multiple: true
+    size: large
+    label: PLUGIN_ADMIN.PERMISSIONS
+    classes: fancy
+    help: PLUGIN_ADMIN.PERMISSIONS_HELP
+    validate:
+        type: commalist
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_ADD_PERMISSIONS
+    name: bulk_add_acl
+    style: vertical
+    classes: large
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_REMOVE_PERMISSIONS
+    name: bulk_remove_acl
+    style: vertical
+    classes: large warning
+
+bulk_group:
+  fields:
+  - type: section
+    title: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_DELETE_GROUP
+
+  - type: button
+    text: PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_DELETE
+    name: bulk_delete
+    style: vertical
+    classes: large warning

+ 108 - 0
user/plugins/admin-addon-user-manager/src/Dot.php

@@ -0,0 +1,108 @@
+<?php
+namespace AdminAddonUserManager;
+
+/**
+ * Class Dot
+ *
+ * @package SelvinOrtiz\Dot
+ *
+ * https://github.com/selvinortiz/dot
+ */
+class Dot
+{
+    /**
+     * Returns whether or not the $key exists within $arr
+     *
+     * @param array  $arr
+     * @param string $key
+     *
+     * @return bool
+     */
+    public static function has($arr, $key)
+    {
+        if (strpos($key, '.') !== false && count(($keys = explode('.', $key)))) {
+            foreach ($keys as $key) {
+                if (!array_key_exists($key, $arr)) {
+                    return false;
+                }
+
+                $arr = $arr[$key];
+            }
+
+            return true;
+        }
+
+        return array_key_exists($key, $arr);
+    }
+
+    /**
+     * Returns he value of $key if found in $arr or $default
+     *
+     * @param array       $arr
+     * @param string      $key
+     * @param null|mixed  $default
+     *
+     * @return mixed
+     */
+    public static function get($arr, $key, $default = null)
+    {
+        if (strpos($key, '.') !== false && count(($keys = explode('.', $key)))) {
+            foreach ($keys as $key) {
+                if (!array_key_exists($key, $arr)) {
+                    return $default;
+                }
+
+                $arr = $arr[$key];
+            }
+
+            return $arr;
+        }
+
+        return array_key_exists($key, $arr) ? $arr[$key] : $default;
+    }
+
+    /**
+     * Sets the $value identified by $key inside $arr
+     *
+     * @param array  &$arr
+     * @param string $key
+     * @param mixed  $value
+     */
+    public static function set(array &$arr, $key, $value)
+    {
+        if (strpos($key, '.') !== false && ($keys = explode('.', $key)) && count($keys)) {
+            while (count($keys) > 1) {
+                $key = array_shift($keys);
+
+                if (!isset($arr[$key]) || !is_array($arr[$key])) {
+                    $arr[$key] = [];
+                }
+
+                $arr = &$arr[$key];
+            }
+
+            $arr[array_shift($keys)] = $value;
+        } else {
+            $arr[$key] = $value;
+        }
+    }
+
+    /**
+     * Deletes a $key and its value from the $arr
+     *
+     * @param  array &$arr
+     * @param string $key
+     */
+    public static function delete(array &$arr, $key)
+    {
+        if (strpos($key, '.') !== false && ($keys = explode('.', $key)) && count($keys)) {
+            while (count($keys) > 1) {
+                $arr = &$arr[array_shift($keys)];
+            }
+
+            unset($arr[array_shift($keys)]);
+        } else {
+            unset($arr[$key]);
+        }
+    }
+}

+ 164 - 0
user/plugins/admin-addon-user-manager/src/Group.php

@@ -0,0 +1,164 @@
+<?php
+// Copied from Grav source because it has issues which has been fixed here
+
+namespace AdminAddonUserManager;
+
+use Grav\Common\Data\Blueprints;
+use Grav\Common\Data\Data;
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Grav;
+use Grav\Common\Utils;
+
+class Group extends Data {
+
+  /**
+   * Get the groups list
+   *
+   * @return array
+   */
+  public static function groups() {
+    $groups = Grav::instance()['config']->get('groups', []);
+
+    $blueprints = new Blueprints;
+    $blueprint = $blueprints->get('user/group');
+    foreach ($groups as $groupname => &$content) {
+      if (!isset($content['groupname'])) {
+        $content['groupname'] = $groupname;
+      }
+      $content = new Group($content, $blueprint);
+    }
+
+    return $groups;
+  }
+
+  /**
+   * Get the groups list
+   *
+   * @return array
+   */
+  public static function groupNames() {
+    $groups = [];
+
+    foreach(Grav::instance()['config']->get('groups', []) as $groupname => $group) {
+      $groups[$groupname] = isset($group['readableName']) ? $group['readableName'] : $groupname;
+    }
+
+    return $groups;
+  }
+
+  /**
+   * Checks if a group exists
+   *
+   * @param string $groupname
+   *
+   * @return bool
+   */
+  public static function groupExists($groupname) {
+    return isset(self::groups()[$groupname]);
+  }
+
+  /**
+   * Get a group by name
+   *
+   * @param string $groupname
+   *
+   * @return object
+   */
+  public static function load($groupname) {
+    if (self::groupExists($groupname)) {
+      $group = self::groups()[$groupname];
+    } else {
+      $blueprints = new Blueprints;
+      $blueprint = $blueprints->get('user/group');
+      $content = ['groupname' => $groupname];
+      $group = new Group($content, $blueprint);
+    }
+
+    return $group;
+  }
+
+  /**
+   * Save a group
+   */
+  public function save() {
+    $grav = Grav::instance();
+    $config = $grav['config'];
+
+    $blueprints = new Blueprints;
+    $blueprint = $blueprints->get('user/group');
+
+    $fields = $blueprint->fields();
+
+    $config->set("groups.$this->groupname", []);
+
+    foreach ($fields as $field) {
+      if ($field['type'] == 'text') {
+        $value = $field['name'];
+        if (isset($this->items[$value])) {
+          $config->set("groups.$this->groupname.$value", $this->items[$value]);
+        }
+      }
+
+      if ($field['type'] == 'array' || $field['type'] == 'permissions') {
+        $value = $field['name'];
+        $arrayValues = Utils::getDotNotation($this->items, $field['name']);
+
+        if ($arrayValues) {
+          foreach ($arrayValues as $arrayIndex => $arrayValue) {
+            $config->set("groups.$this->groupname.$value.$arrayIndex", $arrayValue);
+          }
+        }
+      }
+    }
+
+    $type = 'groups';
+    $obj = new Data($config->get($type), $blueprint);
+    $file = CompiledYamlFile::instance($grav['locator']->findResource('config://') . DS . "{$type}.yaml");
+    $obj->file($file);
+    $obj->save();
+  }
+
+  /**
+   * Remove a group
+   *
+   * @param string $groupname
+   *
+   * @return bool True if the action was performed
+   */
+  public static function remove($groupname) {
+    $grav = Grav::instance();
+    $config = $grav['config'];
+    $blueprints = new Blueprints;
+    $blueprint = $blueprints->get('user/group');
+
+    $groups = $config->get('groups', []);
+    if (!isset($groups[$groupname])) {
+      return false;
+    }
+    unset($groups[$groupname]);
+    $config->set('groups', $groups);
+
+    $type = 'groups';
+    $obj = new Data($config->get($type), $blueprint);
+    $file = CompiledYamlFile::instance($grav['locator']->findResource("config://{$type}.yaml"));
+    $obj->file($file);
+    $obj->save();
+
+    return true;
+  }
+
+  public function authorize($access) {
+    if (empty($this->items)) {
+      return false;
+    }
+
+    if (!isset($this->items['access'])) {
+      return false;
+    }
+
+    $val = Utils::getDotNotation($this->items['access'], $access);
+
+    return Utils::isPositive($val) === true;
+  }
+
+}

+ 242 - 0
user/plugins/admin-addon-user-manager/src/Groups/Manager.php

@@ -0,0 +1,242 @@
+<?php
+namespace AdminAddonUserManager\Groups;
+
+use Grav\Common\Grav;
+use Grav\Plugin\AdminAddonUserManagerPlugin;
+use Grav\Common\Assets;
+use RocketTheme\Toolbox\Event\Event;
+use AdminAddonUserManager\Manager as IManager;
+use AdminAddonUserManager\Pagination\ArrayPagination;
+use \Grav\Common\Utils;
+use AdminAddonUserManager\Group;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use \Grav\Common\User\User;
+use \AdminAddonUserManager\Users\Manager as UsersManager;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\ExpressionLanguage\ExpressionFunction;
+
+class Manager implements IManager, EventSubscriberInterface {
+
+  private $grav;
+  private $plugin;
+  private $adminController;
+
+  public function __construct(Grav $grav, AdminAddonUserManagerPlugin $plugin) {
+    $this->grav = $grav;
+    $this->plugin = $plugin;
+
+    $this->grav['events']->addSubscriber($this);
+  }
+
+  public static function getSubscribedEvents() {
+    return [
+      'onAdminControllerInit' => ['onAdminControllerInit', 0],
+      'onAdminData' => ['onAdminData', 0]
+    ];
+  }
+
+  public function onAdminControllerInit($e) {
+    $controller = $e['controller'];
+    $this->adminController = $controller;
+  }
+
+  public function onAdminData($e) {
+    $type = $e['type'];
+
+    if (preg_match('|group-manager|', $type) && ($group = $this->grav['uri']->param('name', false))) {
+      $obj = Group::load($group);
+      $post = $this->adminController->data;
+      if (isset($post['users'])) {
+        $usersInGroup = $post['users'];
+        unset($post['users']);
+      } else {
+        $usersInGroup = [];
+      }
+      $obj->merge($post);
+      $e['data_type'] = $obj;
+
+      foreach (UsersManager::$instance->users() as $u) {
+        $groups = $u->get('groups', []);
+        if (in_array($u['username'], $usersInGroup)) {
+          if (!in_array($obj['groupname'], $groups)) {
+            $u['groups'] = array_merge($groups, [$obj['groupname']]);
+            $u->save();
+          }
+        } else {
+          if (in_array($obj['groupname'], $groups)) {
+            $u['groups'] = array_diff($groups, [$obj['groupname']]);
+            if (empty($u['groups'])) {
+              unset($u['groups']);
+            }
+            $u->save();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns the required permission to access the manager
+   *
+   * @return string
+   */
+  public function getRequiredPermission() {
+    return $this->plugin->name . '.groups';
+  }
+
+  /**
+   * Returns the location of the manager
+   * It will be accessible at this path
+   *
+   * @return string
+   */
+  public function getLocation() {
+    return 'group-manager';
+  }
+
+  /**
+   * Returns the plugin hooked nav array
+   *
+   * @return array
+   */
+  public function getNav() {
+    return [
+      'label' => 'PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUP_MANAGER',
+      'location' => $this->getLocation(),
+      'icon' => 'fa-group',
+      'authorize' => $this->getRequiredPermission(),
+      'badge' => [
+        'count' => count($this->groups())
+      ]
+    ];
+  }
+
+  /**
+   * Initialiaze required assets
+   *
+   * @param \Grav\Common\Assets $assets
+   * @return void
+   */
+  public function initializeAssets(Assets $assets) {
+    $this->grav['assets']->addCss('plugin://' . $this->plugin->name . '/assets/groups/style.css');
+  }
+
+  /**
+   * Handle task requests
+   *
+   * @param \RocketTheme\Toolbox\Event\Event $event
+   * @return boolean
+   */
+  public function handleTask(Event $event) {
+    $method = $event['method'];
+
+    if ($method === 'taskGroupDelete' && ($group = $this->grav['uri']->param('name', false))) {
+      Group::remove($group);
+      $this->grav->redirect($this->grav['uri']->url($this->getLocation()));
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Logic of the manager goes here
+   *
+   * @return array The array to be merged to Twig vars
+   */
+  public function handleRequest() {
+    $vars = [];
+
+    $twig = $this->grav['twig'];
+    $uri = $this->grav['uri'];
+
+    // Bulk actions
+    if (isset($_POST['selected'])) {
+      $groupnames = $_POST['selected'];
+
+      if (isset($_POST['bulk_delete'])) {
+        // Bulk delete groups
+        foreach ($groupnames as $groupname) {
+          Group::remove($groupname);
+        }
+
+        $this->grav->redirect($this->plugin->getPreviousUrl());
+      }
+    }
+
+    $group = $this->grav['uri']->param('name', false);
+
+    if ($group) {
+      $vars['exists'] = Group::groupExists($group);
+      $vars['group'] = $group = Group::load($group);
+      $users = [];
+      foreach (UsersManager::$instance->users() as $u) {
+        if (in_array($group['groupname'], $u->get('groups', []))) {
+          $users[] = $u->username;
+        }
+      }
+      $group['users'] = $users;
+    } else {
+      $vars['fields'] = $this->plugin->getModalsConfiguration()['add_group']['fields'];
+      $vars['bulkFields'] = $this->plugin->getModalsConfiguration()['bulk_group']['fields'];
+
+      $groups = $this->groups();
+      foreach ($groups as &$group) {
+        $group['users'] = 0;
+
+        foreach (UsersManager::$instance->users() as $u) {
+          if (in_array($group['groupname'], $u->get('groups', []))) {
+            $group['users'] += 1;
+          }
+        }
+      }
+
+      // Filtering
+      $filterException = false;
+      $filter = (empty($_GET['filter'])) ? '' : $_GET['filter'];
+      $vars['filter'] = $filter;
+      if ($filter) {
+        try {
+          $language = new ExpressionLanguage();
+          $language->addFunction(ExpressionFunction::fromPhp('count'));
+          foreach ($groups as $k => $group) {
+            if (!$language->evaluate($_GET['filter'], ['group' => $group])) {
+              unset($groups[$k]);
+            }
+          }
+        } catch (\Exception $exception) {
+          $vars['filterException'] = $exception;
+          $filterException = true;
+        }
+      }
+
+      if ($filterException) {
+        $groups = [];
+      }
+
+      // Pagination
+      $perPage = $this->plugin->getPluginConfigValue('pagination.per_page', 10);
+      $pagination = new ArrayPagination($groups, $perPage);
+      $pagination->paginate($uri->param('page'));
+
+      $vars['pagination'] = [
+        'current' => $pagination->getCurrentPage(),
+        'count' => $pagination->getPagesCount(),
+        'total' => $pagination->getRowsCount(),
+        'perPage' => $pagination->getRowsPerPage(),
+        'startOffset' => $pagination->getStartOffset(),
+        'endOffset' => $pagination->getEndOffset()
+      ];
+      $groups = $pagination->getPaginatedRows();
+
+      $vars['groups'] = $groups;
+    }
+
+    return $vars;
+  }
+
+  public function groups() {
+    return Group::groups();
+  }
+
+}

+ 58 - 0
user/plugins/admin-addon-user-manager/src/Manager.php

@@ -0,0 +1,58 @@
+<?php
+namespace AdminAddonUserManager;
+
+use Grav\Common\Grav;
+use Grav\Plugin\AdminAddonUserManagerPlugin;
+use Grav\Common\Assets;
+use RocketTheme\Toolbox\Event\Event;
+
+interface Manager {
+
+  public function __construct(Grav $grav, AdminAddonUserManagerPlugin $plugin);
+
+  /**
+   * Returns the required permission to access the manager
+   *
+   * @return string
+   */
+  public function getRequiredPermission();
+
+  /**
+   * Returns the location of the manager
+   * It will be accessible at this path
+   *
+   * @return string
+   */
+  public function getLocation();
+
+  /**
+   * Returns the plugin hooked nav array
+   *
+   * @return array
+   */
+  public function getNav();
+
+  /**
+   * Initialiaze required assets
+   *
+   * @param \Grav\Common\Assets $assets
+   * @return void
+   */
+  public function initializeAssets(Assets $assets);
+
+  /**
+   * Handle task requests
+   *
+   * @param \RocketTheme\Toolbox\Event\Event $event
+   * @return void
+   */
+  public function handleTask(Event $event);
+
+  /**
+   * Logic of the manager goes here
+   *
+   * @return array The array to be merged to Twig vars
+   */
+  public function handleRequest();
+
+}

+ 81 - 0
user/plugins/admin-addon-user-manager/src/Pagination/ArrayPagination.php

@@ -0,0 +1,81 @@
+<?php
+namespace AdminAddonUserManager\Pagination;
+
+use AdminAddonUserManager\Pagination\Pagination;
+
+class ArrayPagination implements Pagination {
+
+  protected $data;
+  protected $rowsPerPage;
+  protected $page;
+
+  private $rowsCount = null;
+  private $slicedData = null;
+
+  public function __construct($data, $rowsPerPage = 10) {
+    $this->data = $data;
+    $this->rowsPerPage = $rowsPerPage;
+    $this->page = 1;
+  }
+
+  public function paginate($page) {
+    if ($page > $this->getPagesCount()) {
+      $page = $page;
+    }
+
+    if ($page < 1) {
+      $page = 1;
+    }
+
+    $this->page = $page;
+
+    $this->slicedData = null;
+  }
+
+  public function getRowsPerPage() {
+    return $this->rowsPerPage;
+  }
+
+  public function getRowsCount() {
+    if ($this->rowsCount !== null) {
+      return $this->rowsCount;
+    }
+
+    return $this->rowsCount = count($this->data);
+  }
+
+  public function getCurrentPage() {
+    return $this->page;
+  }
+
+  public function getPagesCount() {
+    return ceil($this->getRowsCount() / $this->getRowsPerPage());
+  }
+
+  public function getStartOffset() {
+    return ($this->getCurrentPage() - 1) * $this->getRowsPerPage();
+  }
+
+  public function getEndOffset() {
+    $endOffset = $this->getStartOffset() + $this->getRowsPerPage();
+
+    if ($endOffset > $this->getRowsCount()) {
+      $endOffset = $this->getRowsCount();
+    }
+
+    return $endOffset;
+  }
+
+  public function getPaginatedRowsCount() {
+    return $this->getEndOffset() - $this->getStartOffset();
+  }
+
+  public function getPaginatedRows() {
+    if ($this->slicedData !== null) {
+      return $this->slicedData;
+    }
+
+    return $this->slicedData = array_slice($this->data, $this->getStartOffset(), $this->getRowsPerPage());
+  }
+
+}

+ 17 - 0
user/plugins/admin-addon-user-manager/src/Pagination/Pagination.php

@@ -0,0 +1,17 @@
+<?php
+namespace AdminAddonUserManager\Pagination;
+
+interface Pagination {
+
+  public function paginate($page);
+
+  public function getRowsPerPage();
+  public function getRowsCount();
+  public function getCurrentPage();
+  public function getPagesCount();
+  public function getStartOffset();
+  public function getEndOffset();
+  public function getPaginatedRowsCount();
+  public function getPaginatedRows();
+
+}

+ 100 - 0
user/plugins/admin-addon-user-manager/src/Users/ExpertManager.php

@@ -0,0 +1,100 @@
+<?php
+namespace AdminAddonUserManager\Users;
+
+use Grav\Common\Grav;
+use Grav\Plugin\AdminAddonUserManagerPlugin;
+use Grav\Common\Assets;
+use RocketTheme\Toolbox\Event\Event;
+use AdminAddonUserManager\Manager as IManager;
+use Grav\Common\User\User;
+use Grav\Common\Data\Blueprints;
+
+class ExpertManager implements IManager {
+
+  private $grav;
+  private $plugin;
+
+  public static $instance;
+
+  public function __construct(Grav $grav, AdminAddonUserManagerPlugin $plugin) {
+    $this->grav = $grav;
+    $this->plugin = $plugin;
+
+    self::$instance = $this;
+  }
+
+  /**
+   * Returns the required permission to access the manager
+   *
+   * @return string
+   */
+  public function getRequiredPermission() {
+    return $this->plugin->name . '.users_expert';
+  }
+
+  /**
+   * Returns the location of the manager
+   * It will be accessible at this path
+   *
+   * @return string
+   */
+  public function getLocation() {
+    return 'user-expert';
+  }
+
+  /**
+   * Returns the plugin hooked nav array
+   *
+   * @return array
+   */
+  public function getNav() {
+    return false;
+  }
+
+  /**
+   * Initialiaze required assets
+   *
+   * @param \Grav\Common\Assets $assets
+   * @return void
+   */
+  public function initializeAssets(Assets $assets) {
+    $assets->addCss('plugin://admin/themes/grav/css/codemirror/codemirror.css');
+  }
+
+  /**
+   * Handle task requests
+   *
+   * @param \RocketTheme\Toolbox\Event\Event $event
+   * @return boolean
+   */
+  public function handleTask(Event $event) {}
+
+  /**
+   * Logic of the manager goes here
+   *
+   * @return array The array to be merged to Twig vars
+   */
+  public function handleRequest() {
+    $vars = [];
+
+    $twig = $this->grav['twig'];
+    $uri = $this->grav['uri'];
+
+    $username = $uri->paths()[2];
+    $user = $this->grav['accounts']->load($username);
+
+    if (isset($_POST['raw'])) {
+      $user->file()->raw($_POST['raw']);
+      $user->file()->save();
+    }
+
+    $vars['raw'] = $user->file()->raw();
+    $vars['user'] = $user;
+
+    $blueprints = new Blueprints;
+    $vars['blueprint'] = $blueprints->get('user/account-raw');
+
+    return $vars;
+  }
+
+}

+ 432 - 0
user/plugins/admin-addon-user-manager/src/Users/Manager.php

@@ -0,0 +1,432 @@
+<?php
+namespace AdminAddonUserManager\Users;
+
+use Grav\Common\Grav;
+use Grav\Plugin\AdminAddonUserManagerPlugin;
+use Grav\Common\Assets;
+use Grav\Common\Data\Blueprints;
+use RocketTheme\Toolbox\Event\Event;
+use AdminAddonUserManager\Manager as IManager;
+use AdminAddonUserManager\Pagination\ArrayPagination;
+use Grav\Common\Utils;
+use Grav\Common\User\User;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\ExpressionLanguage\ExpressionFunction;
+use AdminAddonUserManager\Group;
+use AdminAddonUserManager\Dot;
+
+class Manager implements IManager, EventSubscriberInterface {
+
+  private $grav;
+  private $plugin;
+  private $adminController;
+
+  /**
+   * In-memory caching for users
+   *
+   * @var Array<User>
+   */
+  private $usersCached = null;
+
+  /**
+   * In-memory cache of the account directory
+   *
+   * @var String
+   */
+  private $accountDirCached = null;
+
+  public static $instance;
+
+  public function __construct(Grav $grav, AdminAddonUserManagerPlugin $plugin) {
+    $this->grav = $grav;
+    $this->plugin = $plugin;
+
+    self::$instance = $this;
+
+    $this->grav['events']->addSubscriber($this);
+  }
+
+  public static function getSubscribedEvents() {
+    return [
+      'onAdminControllerInit' => ['onAdminControllerInit', 0],
+      'onAdminData' => ['onAdminData', 0]
+    ];
+  }
+
+  public function onAdminControllerInit($e) {
+    $controller = $e['controller'];
+    $this->adminController = $controller;
+  }
+
+  public function onAdminData($e) {
+    $type = $e['type'];
+
+    if (preg_match('|user-manager/|', $type)) {
+      $post = $this->adminController->data;
+
+      $obj = $this->grav['accounts']->load(preg_replace('|user-manager/|', '', $type));
+      $obj->merge($post);
+
+      $e['data_type'] = $obj;
+    }
+  }
+
+  /**
+   * Returns the required permission to access the manager
+   *
+   * @return string
+   */
+  public function getRequiredPermission() {
+    return $this->plugin->name . '.users';
+  }
+
+  /**
+   * Returns the location of the manager
+   * It will be accessible at this path
+   *
+   * @return string
+   */
+  public function getLocation() {
+    return 'user-manager';
+  }
+
+  /**
+   * Returns the plugin hooked nav array
+   *
+   * @return array
+   */
+  public function getNav() {
+    return [
+      'label' => 'PLUGIN_ADMIN_ADDON_USER_MANAGER.USER_MANAGER',
+      'location' => $this->getLocation(),
+      'icon' => 'fa-user',
+      'authorize' => $this->getRequiredPermission(),
+      'badge' => [
+        'count' => count($this->users())
+      ]
+    ];
+  }
+
+  /**
+   * Initialiaze required assets
+   *
+   * @param \Grav\Common\Assets $assets
+   * @return void
+   */
+  public function initializeAssets(Assets $assets) {
+    $assets->addCss('plugin://' . $this->plugin->name . '/assets/users/style.css');
+  }
+
+  /**
+   * Handle task requests
+   *
+   * @param \RocketTheme\Toolbox\Event\Event $event
+   * @return boolean
+   */
+  public function handleTask(Event $event) {
+    $method = $event['method'];
+
+    if ($method === 'taskUserDelete') {
+      $username = $this->grav['uri']->paths()[2];
+      if ($this->removeUser($username)) {
+        $this->adminController->setRedirect($this->getLocation());
+      }
+    } elseif ($method === 'taskUserLoginAs') {
+      $username = $this->grav['uri']->paths()[2];
+      $user = $this->grav['accounts']->load($username);
+      $user->authenticated = true;
+
+      $this->grav['session']->user = $user;
+      unset($this->grav['user']);
+      $this->grav['user'] = $user;
+
+      if ($user->authorize('admin.login')) {
+        $this->adminController->setRedirect('/');
+      } else {
+        $this->grav->redirect('/');
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Logic of the manager goes here
+   *
+   * @return array The array to be merged to Twig vars
+   */
+  public function handleRequest() {
+    $vars = [];
+
+    $twig = $this->grav['twig'];
+    $uri = $this->grav['uri'];
+
+    $user = $this->grav['uri']->paths();
+    if (count($user) == 3) {
+      $user = $user[2];
+    } else {
+      $user = false;
+    }
+
+    if ($user) {
+      if (isset($_POST['task']) && $_POST['task'] === 'admin-addon-user-manager-save') {
+        $user = $this->grav['accounts']->load($user);
+        $post = $_POST['data'];
+
+        try {
+          $user->merge($post);
+          $method = new \ReflectionMethod('\Grav\Plugin\Admin\AdminController', 'storeFiles');
+          $method->setAccessible(true);
+          $user = $method->invoke($this->adminController, $user);
+          $user->validate();
+          $user->filter();
+          $user->save();
+        } catch (\Exception $e) {
+          $this->grav['admin']->setMessage($e->getMessage(), 'error');
+        }
+
+        $this->grav->redirect($this->plugin->getPreviousUrl());
+      } else {
+        $blueprints = new Blueprints;
+        $blueprint = $blueprints->get('user/aaum-account');
+        $vars['blueprints'] = $blueprint;
+        $vars['user'] = $user = $this->grav['accounts']->load($user);
+        $vars['exists'] = $user->exists();
+      }
+    } else {
+      // Bulk actions
+      if (isset($_POST['selected'])) {
+        $usernames = $_POST['selected'];
+
+        if (isset($_POST['bulk_delete'])) {
+          // Bulk delete
+          foreach ($usernames as $username) {
+            $this->removeUser($username);
+          }
+
+          $this->grav->redirect($this->plugin->getPreviousUrl());
+        } else if (isset($_POST['bulk_add_to_group']) && isset($_POST['groups'])) {
+          // Bulk add users to groups
+          $groups = $_POST['groups'];
+
+          foreach ($usernames as $username) {
+            $user = $this->grav['accounts']->load($username);
+            if ($user->file()->exists()) {
+              if (!isset($user['groups']) || !is_array($user['groups'])) {
+                $user['groups'] = [];
+              }
+
+              $user['groups'] = array_unique(array_merge($user['groups'], $groups));
+              $user->save();
+            }
+          }
+
+          $this->grav->redirect($this->plugin->getPreviousUrl());
+        } else if (isset($_POST['bulk_remove_from_group']) && isset($_POST['groups'])) {
+          // Bulk remove users from groups
+          $groups = $_POST['groups'];
+
+          foreach ($usernames as $username) {
+            $user = $this->grav['accounts']->load($username);
+            if ($user->file()->exists()) {
+              if (!isset($user['groups']) || !is_array($user['groups'])) {
+                $user['groups'] = [];
+              }
+
+              $user['groups'] = array_unique(array_diff($user['groups'], $groups));
+              $user->save();
+            }
+          }
+
+          $this->grav->redirect($this->plugin->getPreviousUrl());
+        } else if (isset($_POST['bulk_add_acl']) && isset($_POST['permissions'])) {
+          // Bulk add permissions to users
+          $access = [];
+          foreach ($_POST['permissions'] as $p) {
+            Dot::set($access, $p, true);
+          }
+
+          foreach ($usernames as $username) {
+            $user = $this->grav['accounts']->load($username);
+            if ($user->file()->exists()) {
+              if (!isset($user['access']) || !is_array($user['access'])) {
+                $user['access'] = [];
+              }
+
+              $user['access'] = array_merge_recursive($user['access'], $access);
+              $user->save();
+            }
+          }
+
+          $this->grav->redirect($this->plugin->getPreviousUrl());
+        } else if (isset($_POST['bulk_remove_acl']) && isset($_POST['permissions'])) {
+          // Bulk remove permissions from users
+          foreach ($usernames as $username) {
+            $user = $this->grav['accounts']->load($username);
+            if ($user->file()->exists()) {
+              if (!isset($user['access']) || !is_array($user['access'])) {
+                $user['access'] = [];
+              }
+
+              $access = $user['access'];
+              foreach ($_POST['permissions'] as $p) {
+                Dot::delete($access, $p);
+              }
+              $user['access'] = $access;
+              $user->save();
+            }
+          }
+
+          $this->grav->redirect($this->plugin->getPreviousUrl());
+        }
+      }
+
+      $vars['fields'] = $this->plugin->getModalsConfiguration()['add_user']['fields'];
+      $vars['bulkFields'] = $this->plugin->getModalsConfiguration()['bulk_user']['fields'];
+      $vars['groupnames'] = Group::groupNames();
+      $permissions = array_keys($this->grav['admin']->getPermissions());
+      foreach ($permissions as $k=>&$v) $v = ['text' => $v, 'value' => $v];
+      $vars['permissions'] = $permissions;
+
+      // List style (grid or list)
+      $listStyle = $uri->param('listStyle');
+      if ($listStyle !== 'grid' && $listStyle !== 'list') {
+        $listStyle = $this->plugin->getPluginConfigValue('default_list_style', 'grid');
+      }
+      $vars['listStyle'] = $listStyle;
+
+      $users = $this->users();
+
+      // Filtering
+      $filterException = false;
+      $filter = (empty($_GET['filter'])) ? '' : $_GET['filter'];
+      $vars['filter'] = $filter;
+      if ($filter) {
+        try {
+          $language = new ExpressionLanguage();
+          $language->addFunction(ExpressionFunction::fromPhp('count'));
+          foreach ($users as $k => $user) {
+            if (!is_array($user->groups)) {
+              $user->groups = [];
+            }
+
+            if (!$language->evaluate($_GET['filter'], ['user' => $user])) {
+              unset($users[$k]);
+            }
+          }
+        } catch (\Exception $exception) {
+          $vars['filterException'] = $exception;
+          $filterException = true;
+        }
+      }
+
+      if ($filterException) {
+        $users = [];
+      }
+
+      // Pagination
+      $perPage = $this->plugin->getPluginConfigValue('pagination.per_page', 10);
+      $pagination = new ArrayPagination($users, $perPage);
+      $pagination->paginate($uri->param('page'));
+
+      $vars['pagination'] = [
+        'current' => $pagination->getCurrentPage(),
+        'count' => $pagination->getPagesCount(),
+        'total' => $pagination->getRowsCount(),
+        'perPage' => $pagination->getRowsPerPage(),
+        'startOffset' => $pagination->getStartOffset(),
+        'endOffset' => $pagination->getEndOffset()
+      ];
+      $vars['users'] = $pagination->getPaginatedRows();
+      $vars['user'] = false;
+    }
+
+    return $vars;
+  }
+
+  public function users() {
+    if ($this->usersCached) {
+      return $this->usersCached;
+    }
+
+    $users = [];
+    $dir = $this->getAccountDir();
+
+    // Try cache
+    $cache =  $this->grav['cache'];
+    $cacheKey = $this->plugin->name . '.users';
+
+    $modifyTime = filemtime($dir);
+    $usersCache = $cache->fetch($cacheKey);
+    if (!$usersCache || $modifyTime > $usersCache['modifyTime']) {
+      // Find accounts
+      $files = $dir ? array_diff(scandir($dir), ['.', '..']) : [];
+      foreach ($files as $file) {
+        if (Utils::endsWith($file, YAML_EXT)) {
+          $user = $this->grav['accounts']->load(trim(pathinfo($file, PATHINFO_FILENAME)));
+          $users[$user->username] = $user;
+        }
+      }
+
+      // Populate and/or refresh cache
+      $this->saveUsersToCache($users);
+    } else {
+      $users = $usersCache['users'];
+    }
+
+    $this->usersCached = $users;
+
+    return $users;
+  }
+
+  private function saveUsersToCache($users) {
+    $cache =  $this->grav['cache'];
+    $cacheKey = $this->plugin->name . '.users';
+    $dir = $this->getAccountDir();
+    $modifyTime = filemtime($dir);
+
+    $usersCache = [
+      'modifyTime' => $modifyTime,
+      'users' => $users,
+    ];
+
+    $cache->save($cacheKey, $usersCache);
+  }
+
+  private function getAccountDir() {
+    if ($this->accountDirCached) {
+      return $this->grav['locator']->findResource('account://');
+    }
+
+    return $this->accountDirCached = $this->grav['locator']->findResource('account://');
+  }
+
+  public function removeUser($username) {
+    $user = $this->grav['accounts']->load($username);
+
+    if ($user->file()->exists()) {
+      $users = $this->users();
+      $user->file()->delete();
+      // Prevent users cache refresh
+      unset($users[$username]);
+      $this->saveUsersToCache($users);
+      return true;
+    }
+
+    return false;
+  }
+
+  public static function userNames() {
+    $instance = self::$instance;
+    $users = $instance->users();
+
+    $userNames = [];
+    foreach ($users as $u) {
+      $userNames[$u['username']] = $u['username'];
+    }
+
+    return $userNames;
+  }
+
+}

+ 25 - 0
user/plugins/admin-addon-user-manager/templates/forms/fields/button/button.html.twig

@@ -0,0 +1,25 @@
+{% extends "forms/field.html.twig" %}
+
+{% block input %}
+    <div class="form-input-wrapper {{ field.size }}">
+        <button
+                name="{{ (scope ~ field.name)|fieldName }}"
+                type="submit"
+                {% block input_attributes %}
+                    {% if field.classes is defined %}class="{{ field.classes }} button" {% endif %}
+                    {% if field.id is defined %}id="{{ field.id|e }}" {% endif %}
+                    {% if field.style is defined %}style="{{ field.style|e }}" {% endif %}
+                    {% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
+                    {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
+                    {% if field.autofocus in ['on', 'true', 1] %}autofocus="autofocus"{% endif %}
+                    {% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %}
+                    {% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %}
+                    {% if field.autocomplete in ['on', 'off'] %}autocomplete="{{ field.autocomplete }}"{% endif %}
+                    {% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
+                    {% if field.validate.pattern %}pattern="{{ field.validate.pattern }}"{% endif %}
+                    {% if field.validate.message %}title="{{ field.validate.message|e|t }}"
+                    {% elseif field.title is defined %}title="{{ field.title|e|t }}" {% endif %}
+                {% endblock %}
+        >{{ field.text|t }}</button>
+    </div>
+{% endblock %}

+ 170 - 0
user/plugins/admin-addon-user-manager/templates/group-manager.html.twig

@@ -0,0 +1,170 @@
+{% extends 'partials/base.html.twig' %}
+{% import 'user-manager-macros.html.twig' as macros %}
+
+{% set title = "PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUP_MANAGER"|tu %}
+
+{% if group %}
+{% set group_name = group.readableName ?: group.groupname %}
+{% set title = "PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUP"|tu ~ ": " ~ group_name|e %}
+{% endif %}
+
+{% set ps = config.system.param_sep %}
+
+{% block titlebar %}
+  {% if not group %}
+  <div class="button-bar">
+    <a class="button" href="#modal-admin-addon-user-manager-new" data-remodal-target="modal-admin-addon-user-manager-new"><i class="fa fa-plus"></i> {{ "PLUGIN_ADMIN.ADD"|tu }}</a>
+  </div>
+
+  <h1><i class="fa fa-fw fa-user-o"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUP_MANAGER"|tu }}</h1>
+  {% else %}
+  <div class="button-bar">
+    <a class="button" href="{{ base_url }}/group-manager"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
+    {% if exists %}
+    <a class="button disable-after-click" href="{{ uri.addNonce(uri.route(true) ~ '/name' ~ ps ~ group.groupname ~ '/task' ~ ps ~ 'groupDelete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i> {{ "PLUGIN_ADMIN.DELETE"|tu }}</a>
+    {% endif %}
+    <button class="button" type="submit" name="task" value="save" form="blueprints"><i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
+  </div>
+  <h1><i class="fa fa-fw fa-user"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUP"|tu }}: {{ group.groupname|e }}</h1>
+  {% endif %}
+{% endblock %}
+
+{% set appendUrl = '?filter=' ~ filter|url_encode %}
+
+{% block content %}
+  {% if not group %}
+  <h1>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.GROUPS"|tu }}</h1>
+
+  {% if filterException %}
+  <div class="notices red"><h2>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.FILTER_ERROR"|tu }}</h2><p>{{ filterException.message }}</p></div>
+  {% endif %}
+
+  <div class="admin-addon-user-manager-filter">
+    <div class="admin-addon-user-manager-filter__input">
+      <form method="get">
+        <div class="block block-text">
+          <div class="form-field vertical">
+            <div class="form-data" data-grav-default="{{ filter }}">
+              <div class="form-input-wrapper">
+                <input name="filter" value="{{ filter }}" type="text" placeholder="{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.FILTER_GROUPS"|tu }}">
+              </div>
+            </div>
+          </div>
+        </div>
+      </form>
+    </div>
+
+    <div class="admin-addon-user-manager-filter__help">
+      <a href="https://github.com/david-szabo97/grav-plugin-admin-addon-user-manager/blob/master/docs/filter.md#groups" class="button" target="_new"><i class="fa fa-question"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.HELP"|tu }}</a>
+    </div>
+  </div>
+
+  <form method="post">
+    <div class="admin-addon-user-manager admin-addon-user-manager--list admin-addon-user-manager--group">
+      {% if groups is empty %}
+        <h2>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.NO_RESULTS"|tu }}</h2>
+      {% else %}
+        <div class="cell cell--header">
+          <div class="user">
+            <div class="user__checkbox"><input type="checkbox" id="selectAll" /></div>
+            <div class="user__username">{{ "PLUGIN_ADMIN.NAME"|tu }}</div>
+            <div class="user__actions">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.ACTIONS"|tu }}</div>
+          </div>
+        </div>
+
+        {% for groupName, group in groups %}
+        <div class="cell">
+          <div class="user">
+            <div class="user__checkbox"><input type="checkbox" name="selected[]" value="{{ groupName }}" /></div>
+            <div class="user__username"><a href="{{ uri.route(true) ~ '/name' ~ ps ~ group.groupname }}">({{ group.users }}) <i class="fa fa-{{ group.icon}}"></i> {{ group.readableName ?: groupName }}</a></div>
+            <div class="user__actions">
+              <a href="{{ uri.addNonce(uri.route(true)  ~ '/name' ~ ps ~ group.groupname ~ '/task' ~ ps ~ 'groupDelete', 'admin-form', 'admin-nonce') }}" class="delete">{{ "PLUGIN_ADMIN.DELETE"|tu }}</a>
+            </div>
+          </div>
+        </div>
+        {% endfor %}
+      {% endif %}
+    </div>
+
+    <div class="admin-addon-user-manager-bulk-action">
+      <a data-remodal-target="modal-admin-addon-user-manager-bulk" class="button">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_ACTION"|tu }}</a>
+    </div>
+  </form>
+
+  {{ macros.pagination(pagination, uri.route(true), ps, appendUrl) }}
+
+  <div class="remodal" data-remodal-id="modal-admin-addon-user-manager-new" data-remodal-options="hashTracking: false">
+    <form onsubmit='handleAdminAddonUserManagerSubmit(this); return false;'>
+      {% for field in fields %}
+        {% if field.type %}
+          {% set value = data.value(field.name) %}
+          <div class="block block-{{field.type}}">
+            {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+          </div>
+        {% endif %}
+      {% endfor %}
+
+      <div class="button-bar">
+        <button class="button primary" >{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
+      </div>
+    </form>
+  </div>
+
+  <div class="remodal" data-remodal-id="modal-admin-addon-user-manager-bulk" data-remodal-options="hashTracking: false">
+    <form method="post" onsubmit='handleAdminAddonUserManagerBulkSubmit(this);'>
+      {% for field in bulkFields %}
+        {% if field.type %}
+          <div class="block block-{{field.type}}">
+            {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+          </div>
+        {% endif %}
+      {% endfor %}
+
+      <div class="button-bar">
+        <button class="button primary" >{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
+      </div>
+    </form>
+  </div>
+
+  <script>
+    function handleAdminAddonUserManagerSubmit(form) {
+      var name = form.querySelector('[name=name]').value;
+      window.location = '{{ base_url }}/group-manager/name{{ ps }}' + name;
+    }
+
+    var selectAllEle = document.getElementById('selectAll');
+    var selectEles = document.querySelectorAll('input[name="selected[]"]');
+    if (selectAllEle) {
+      selectAllEle.addEventListener('change', function(e) {
+        for (var i = 0; i < selectEles.length; i++) {
+          selectEles[i].checked = this.checked;
+        }
+      });
+    }
+
+    function handleAdminAddonUserManagerBulkSubmit(form) {
+      for (var i = 0; i < selectEles.length; i++) {
+        form.appendChild(selectEles[i]);
+      }
+    }
+  </script>
+  {% else %}
+  <h1>{{ group.readableName ?: group.groupname }}</h1>
+
+  {% include 'partials/blueprints.html.twig' with { data: group, blueprints: group.blueprints } %}
+
+  <div class="remodal" data-remodal-id="changes">
+    <form>
+      <h1>{{ "PLUGIN_ADMIN.MODAL_CHANGED_DETECTED_TITLE"|tu }}</h1>
+      <p class="bigger">
+        {{ "PLUGIN_ADMIN.MODAL_CHANGED_DETECTED_DESC"|tu }}
+      </p>
+      <br>
+      <div class="button-bar">
+        <a class="button secondary" data-leave-action="cancel" href="#"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</a>
+        <a class="button" data-leave-action="continue" href="#"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</a>
+      </div>
+    </form>
+  </div>
+  {% endif %}
+{% endblock %}

+ 28 - 0
user/plugins/admin-addon-user-manager/templates/user-expert.html.twig

@@ -0,0 +1,28 @@
+{% extends 'partials/base.html.twig' %}
+
+{% set title = "PLUGIN_ADMIN.USER"|tu ~ ": " ~ admin.route|e %}
+
+{% block titlebar %}
+  <div class="button-bar">
+    <a class="button" href="{{ base_url }}/user-manager"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
+    <button class="button" type="submit" name="task" value="save" form="blueprints" onclick="document.getElementById('user_expert_form').submit()"><i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
+  </div>
+
+  <h1><i class="fa fa-fw fa-user"></i> {{ "PLUGIN_ADMIN.USER"|tu }}: {{ user.username|e }}</h1>
+{% endblock %}
+
+{% block content %}
+  <h1>{{ title }}</h1>
+
+  <form method="post" id="user_expert_form">
+    {% for field in blueprint.fields %}
+      {% if field.type %}
+        {% set value = attribute(_context, field.name) %}
+        <div class="block block-{{field.type}}">
+          {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+        </div>
+      {% endif %}
+    {% endfor %}
+    {{ nonce_field('form', 'form-nonce')|raw }}
+  </form>
+{% endblock %}

+ 31 - 0
user/plugins/admin-addon-user-manager/templates/user-manager-macros.html.twig

@@ -0,0 +1,31 @@
+{% macro pagination(pagination, url, ps, appendUrl) %}
+  {% if pagination.count > 1 %}
+    <div class="admin-addon-user-manager-pagination">
+      <ul class="admin-addon-user-manager-pagination__pages">
+        {% if pagination.current > 1 %}
+        <li><a href="{{ url ~ '/page' ~ ps ~ '1' ~ appendUrl }}"><<</a></li>
+        {% endif %}
+        {% if pagination.current > 2 %}
+        <li><a href="{{ url ~ '/page' ~ ps ~ (pagination.current - 1) ~ appendUrl }}"><</a></li>
+        {% endif %}
+        {% set fromPage = (pagination.current - 2 < 1) ? 1 : pagination.current - 2 %}
+        {% set toPage = (pagination.current + 2 > pagination.count) ? pagination.count : pagination.current + 2 %}
+        {% for i in fromPage..toPage %}
+          {% if pagination.current == i %}
+            <li class="current">{{ i }}</li>
+          {% else %}
+            <li><a href="{{ url ~ '/page' ~ ps ~ i ~ appendUrl }}">{{ i }}</a></li>
+          {% endif %}
+        {% endfor %}
+        {% if pagination.current < pagination.count - 1 %}
+        <li><a href="{{ url ~ '/page' ~ ps ~ (pagination.current + 1) ~ appendUrl }}">></a></li>
+        {% endif %}
+        {% if pagination.current < pagination.count %}
+        <li><a href="{{ url ~ '/page' ~ ps ~ pagination.count ~ appendUrl }}">>></a></li>
+        {% endif %}
+      </ul>
+
+      <div class="admin-addon-user-manager-pagination__text">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.PAGINATION_SUMMARY"|t(pagination.startOffset + 1, pagination.endOffset, pagination.total) }}</div>
+    </div>
+  {% endif %}
+{% endmacro %}

+ 199 - 0
user/plugins/admin-addon-user-manager/templates/user-manager.html.twig

@@ -0,0 +1,199 @@
+{% extends 'partials/base.html.twig' %}
+{% import 'user-manager-macros.html.twig' as macros %}
+
+{% set title = "PLUGIN_ADMIN_ADDON_USER_MANAGER.USER_MANAGER"|tu %}
+
+{% block titlebar %}
+  {% if not user %}
+  <div class="button-bar">
+    <a class="button" href="#modal-admin-addon-user-manager-new" data-remodal-target="modal-admin-addon-user-manager-new"><i class="fa fa-plus"></i> {{ "PLUGIN_ADMIN.ADD"|tu }}</a>
+  </div>
+
+  <h1><i class="fa fa-fw fa-user-o"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.USER_MANAGER"|tu }}</h1>
+  {% else %}
+  <div class="button-bar">
+    <a class="button" href="{{ base_url }}/user-manager"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
+    {% if exists %}
+    <a class="button disable-after-click" href="{{ uri.addNonce(uri.route(true) ~ '/task' ~ ps ~ 'userDelete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i> {{ "PLUGIN_ADMIN.DELETE"|tu }}</a>
+    <a class="button disable-after-click" href="{{ uri.addNonce(uri.route(true) ~ '/task' ~ ps ~ 'userLoginAs', 'admin-form', 'admin-nonce') }}"><i class="fa fa-sign-in"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.LOGIN_AS"|tu }}</a>
+    {% endif %}
+    <button class="button" type="submit" name="task" value="admin-addon-user-manager-save" form="blueprints"><i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
+  </div>
+  <h1><i class="fa fa-fw fa-user"></i> {{ "PLUGIN_ADMIN.USER"|tu }}: {{ user.username|e }}</h1>
+  {% endif %}
+{% endblock %}
+
+{% set ps = config.system.param_sep %}
+{% set appendUrl = '?filter=' ~ filter|url_encode %}
+
+{% block content %}
+  {% if not user %}
+  {% set style = listStyle|default('grid') %}
+
+  <h1>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.USERS"|tu }}</h1>
+
+  {% if filterException %}
+  <div class="notices red"><h2>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.FILTER_ERROR"|tu }}</h2><p>{{ filterException.message }}</p></div>
+  {% endif %}
+
+  <div class="admin-addon-user-manager-filter">
+    <div class="admin-addon-user-manager-filter__input">
+      <form method="get">
+        <div class="block block-text">
+          <div class="form-field vertical">
+            <div class="form-data" data-grav-default="{{ filter }}">
+              <div class="form-input-wrapper">
+                <input name="filter" value="{{ filter }}" type="text" placeholder="{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.FILTER_USERS"|tu }}">
+              </div>
+            </div>
+          </div>
+        </div>
+      </form>
+    </div>
+
+    <div class="admin-addon-user-manager-filter__help">
+      <a href="https://github.com/david-szabo97/grav-plugin-admin-addon-user-manager/blob/master/docs/filter.md#users" class="button" target="_new"><i class="fa fa-question"></i> {{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.HELP"|tu }}</a>
+    </div>
+  </div>
+
+  <div class="admin-addon-user-manager-style">
+    {% if style != 'grid' %}<a href="{{ uri.route(true) ~ '/listStyle' ~ ps ~ 'grid' ~ '/page' ~ ps ~ pagination.current ~ appendUrl }}"><i class="fa fa-th"></i></a>{% else %}<i class="fa fa-th"></i>{% endif %}
+    {% if style != 'list' %}<a href="{{ uri.route(true) ~ '/listStyle' ~ ps ~ 'list' ~ '/page' ~ ps ~ pagination.current ~ appendUrl }}"><i class="fa fa-list"></i></a>{% else %}<i class="fa fa-list"></i>{% endif %}
+  </div>
+
+  <form method="post">
+    <div class="admin-addon-user-manager admin-addon-user-manager--{{ style }}">
+      {% if users is empty %}
+        <h2>{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.NO_RESULTS"|tu }}</h2>
+      {% else %}
+          {% if style == 'list' %}
+          <div class="cell cell--header">
+            <div class="user">
+              <div class="user__checkbox"><input type="checkbox" id="selectAll" /></div>
+              <div class="user__username">{{ "PLUGIN_ADMIN.USERNAME"|tu }}</div>
+              <div class="user__email">{{ "PLUGIN_ADMIN.EMAIL"|tu }}</div>
+              <div class="user__actions">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.ACTIONS"|tu }}</div>
+            </div>
+          </div>
+          {% endif %}
+          {% for user in users %}
+          <div class="cell">
+            <div class="user">
+              {% if style == 'grid' %}
+              <div class="user__avatar"><a href="{{ base_url }}/user-manager/{{ user.username }}"><img src="{{ user.avatarUrl }}" /></a></div>
+              {% else %}
+              <div class="user__checkbox"><input type="checkbox" name="selected[]" value="{{ user.username }}" /></div>
+              {% endif %}
+              <div class="user__username"><a href="{{ base_url }}/user-manager/{{ user.username }}">{{ user.username }}</a></div>
+              <div class="user__email">{{ user.email }}</div>
+              <div class="user__actions">
+                <a href="{{ base_url ~ '/user-expert/' ~ user.username }}" class="expert">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.EXPERT"|tu }}</a>
+                <a href="{{ uri.addNonce(base_url ~ '/user-manager/' ~ user.username ~ '/task' ~ ps ~ 'userDelete', 'admin-form', 'admin-nonce') }}" class="delete" onclick="return confirm('{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.USER_CONFIRM_DELETE"|tu }}');">{{ "PLUGIN_ADMIN.DELETE"|tu }}</a>
+              </div>
+            </div>
+          </div>
+          {% endfor %}
+      {% endif %}
+    </div>
+
+    <div class="admin-addon-user-manager-bulk-action">
+      <a data-remodal-target="modal-admin-addon-user-manager-bulk" class="button">{{ "PLUGIN_ADMIN_ADDON_USER_MANAGER.BULK_ACTION"|tu }}</a>
+    </div>
+  </form>
+
+  {{ macros.pagination(pagination, uri.route(true) ~ '/listStyle' ~ ps ~ listStyle, ps, appendUrl) }}
+
+  <div class="remodal" data-remodal-id="modal-admin-addon-user-manager-new" data-remodal-options="hashTracking: false">
+    <form method="post" onsubmit='handleAdminAddonUserManagerSubmit(this); return false;'>
+      {% for field in fields %}
+        {% if field.type %}
+          {% if field.name == 'username' %}
+          {% set field = field|merge({ validate: field.validate|merge({ pattern: grav.config.system.username_regex })}) %}
+          {% endif %}
+          <div class="block block-{{field.type}}">
+            {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+          </div>
+        {% endif %}
+      {% endfor %}
+
+      <div class="button-bar">
+        <button class="button primary" >{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
+      </div>
+    </form>
+  </div>
+
+  <div class="remodal" data-remodal-id="modal-admin-addon-user-manager-bulk" data-remodal-options="hashTracking: false">
+    <form method="post" onsubmit='handleAdminAddonUserManagerBulkSubmit(this);'>
+      {% for field in bulkFields %}
+        {% if field.type %}
+          {% if field.name == 'groups' %}
+          {% set field = field|merge({options: groupnames}) %}
+          {% endif %}
+          {% if field.name == 'permissions' %}
+          {% set field = field|merge({selectize: { options: permissions }}) %}
+          {% endif %}
+          <div class="block block-{{field.type}}">
+            {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+          </div>
+        {% endif %}
+      {% endfor %}
+
+      <div class="button-bar">
+        <button class="button primary" >{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
+      </div>
+    </form>
+  </div>
+
+  <script>
+    function handleAdminAddonUserManagerSubmit(form) {
+      var username = form.querySelector('[name=username]').value;
+      window.location = '{{ base_url }}/user-manager/' + username;
+    }
+
+    var selectAllEle = document.getElementById('selectAll');
+    var selectEles = document.querySelectorAll('input[name="selected[]"]');
+    selectAllEle.addEventListener('change', function(e) {
+      for (var i = 0; i < selectEles.length; i++) {
+        selectEles[i].checked = this.checked;
+      }
+    });
+
+    function handleAdminAddonUserManagerBulkSubmit(form) {
+      for (var i = 0; i < selectEles.length; i++) {
+        form.appendChild(selectEles[i]);
+      }
+    }
+  </script>
+  {% else %}
+  <h1>{{ user.username }}</h1>
+
+  {% include 'partials/blueprints.html.twig' with { data: user, blueprints: blueprints } %}
+
+  <div class="remodal" data-remodal-id="changes">
+    <form>
+      <h1>{{ "PLUGIN_ADMIN.MODAL_CHANGED_DETECTED_TITLE"|tu }}</h1>
+      <p class="bigger">
+        {{ "PLUGIN_ADMIN.MODAL_CHANGED_DETECTED_DESC"|tu }}
+      </p>
+      <br>
+      <div class="button-bar">
+        <a class="button secondary" data-leave-action="cancel" href="#"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</a>
+        <a class="button" data-leave-action="continue" href="#"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</a>
+      </div>
+    </form>
+  </div>
+
+  <script>
+    $('[name="task"][value$="save"]').on('click._grav', function(event) {
+      $(global).off('beforeunload');
+    });
+  </script>
+
+  <!-- Temporary fix: https://github.com/getgrav/grav-plugin-admin/pull/1379 -->
+  <style>
+    .permission-container {
+      overflow: hidden;
+    }
+  </style>
+  {% endif %}
+{% endblock %}

+ 7 - 0
user/plugins/admin-addon-user-manager/vendor/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c::getLoader();

+ 445 - 0
user/plugins/admin-addon-user-manager/vendor/composer/ClassLoader.php

@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+    private $classMapAuthoritative = false;
+    private $missingClasses = array();
+    private $apcuPrefix;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}

+ 21 - 0
user/plugins/admin-addon-user-manager/vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 16 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_classmap.php

@@ -0,0 +1,16 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'ArithmeticError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
+    'AssertionError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
+    'DivisionByZeroError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
+    'Error' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/Error.php',
+    'ParseError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
+    'SessionUpdateTimestampHandlerInterface' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
+    'TypeError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
+);

+ 10 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_files.php

@@ -0,0 +1,10 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    '023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
+);

+ 9 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 19 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_psr4.php

@@ -0,0 +1,19 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Symfony\\Polyfill\\Php70\\' => array($vendorDir . '/symfony/polyfill-php70'),
+    'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
+    'Symfony\\Contracts\\Cache\\' => array($vendorDir . '/symfony/cache-contracts'),
+    'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'),
+    'Symfony\\Component\\ExpressionLanguage\\' => array($vendorDir . '/symfony/expression-language'),
+    'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
+    'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
+    'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
+    'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
+    'AdminAddonUserManager\\' => array($baseDir . '/src'),
+);

+ 70 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_real.php

@@ -0,0 +1,70 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require_once __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInit089ad2d043aed688879945cd4bcae11c::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }
+
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }
+
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->register(true);
+
+        if ($useStaticLoader) {
+            $includeFiles = Composer\Autoload\ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$files;
+        } else {
+            $includeFiles = require __DIR__ . '/autoload_files.php';
+        }
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequire089ad2d043aed688879945cd4bcae11c($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}
+
+function composerRequire089ad2d043aed688879945cd4bcae11c($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        require $file;
+
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+    }
+}

+ 97 - 0
user/plugins/admin-addon-user-manager/vendor/composer/autoload_static.php

@@ -0,0 +1,97 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInit089ad2d043aed688879945cd4bcae11c
+{
+    public static $files = array (
+        '023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php',
+    );
+
+    public static $prefixLengthsPsr4 = array (
+        'S' => 
+        array (
+            'Symfony\\Polyfill\\Php70\\' => 23,
+            'Symfony\\Contracts\\Service\\' => 26,
+            'Symfony\\Contracts\\Cache\\' => 24,
+            'Symfony\\Component\\VarExporter\\' => 30,
+            'Symfony\\Component\\ExpressionLanguage\\' => 37,
+            'Symfony\\Component\\Cache\\' => 24,
+        ),
+        'P' => 
+        array (
+            'Psr\\Log\\' => 8,
+            'Psr\\Container\\' => 14,
+            'Psr\\Cache\\' => 10,
+        ),
+        'A' => 
+        array (
+            'AdminAddonUserManager\\' => 22,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'Symfony\\Polyfill\\Php70\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php70',
+        ),
+        'Symfony\\Contracts\\Service\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/service-contracts',
+        ),
+        'Symfony\\Contracts\\Cache\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/cache-contracts',
+        ),
+        'Symfony\\Component\\VarExporter\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/var-exporter',
+        ),
+        'Symfony\\Component\\ExpressionLanguage\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/expression-language',
+        ),
+        'Symfony\\Component\\Cache\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/cache',
+        ),
+        'Psr\\Log\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
+        ),
+        'Psr\\Container\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/container/src',
+        ),
+        'Psr\\Cache\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/cache/src',
+        ),
+        'AdminAddonUserManager\\' => 
+        array (
+            0 => __DIR__ . '/../..' . '/src',
+        ),
+    );
+
+    public static $classMap = array (
+        'ArithmeticError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
+        'AssertionError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
+        'DivisionByZeroError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
+        'Error' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/Error.php',
+        'ParseError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
+        'SessionUpdateTimestampHandlerInterface' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
+        'TypeError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 574 - 0
user/plugins/admin-addon-user-manager/vendor/composer/installed.json

@@ -0,0 +1,574 @@
+[
+    {
+        "name": "paragonie/random_compat",
+        "version": "v9.99.99",
+        "version_normalized": "9.99.99.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/paragonie/random_compat.git",
+            "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+            "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "4.*|5.*",
+            "vimeo/psalm": "^1"
+        },
+        "suggest": {
+            "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+        },
+        "time": "2018-07-02T15:55:56+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Paragon Initiative Enterprises",
+                "email": "security@paragonie.com",
+                "homepage": "https://paragonie.com"
+            }
+        ],
+        "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+        "keywords": [
+            "csprng",
+            "polyfill",
+            "pseudorandom",
+            "random"
+        ]
+    },
+    {
+        "name": "psr/cache",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/cache.git",
+            "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+            "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2016-08-06T20:24:11+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Cache\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for caching libraries",
+        "keywords": [
+            "cache",
+            "psr",
+            "psr-6"
+        ]
+    },
+    {
+        "name": "psr/container",
+        "version": "1.0.0",
+        "version_normalized": "1.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/container.git",
+            "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+            "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2017-02-14T16:28:37+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Container\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common Container Interface (PHP FIG PSR-11)",
+        "homepage": "https://github.com/php-fig/container",
+        "keywords": [
+            "PSR-11",
+            "container",
+            "container-interface",
+            "container-interop",
+            "psr"
+        ]
+    },
+    {
+        "name": "psr/log",
+        "version": "1.1.3",
+        "version_normalized": "1.1.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/log.git",
+            "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+            "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2020-03-23T09:12:05+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.1.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Log\\": "Psr/Log/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for logging libraries",
+        "homepage": "https://github.com/php-fig/log",
+        "keywords": [
+            "log",
+            "psr",
+            "psr-3"
+        ]
+    },
+    {
+        "name": "symfony/cache",
+        "version": "v4.4.7",
+        "version_normalized": "4.4.7.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/cache.git",
+            "reference": "f777b570291aebe51081b9827e05f3a747665e87"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/cache/zipball/f777b570291aebe51081b9827e05f3a747665e87",
+            "reference": "f777b570291aebe51081b9827e05f3a747665e87",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7.1.3",
+            "psr/cache": "~1.0",
+            "psr/log": "~1.0",
+            "symfony/cache-contracts": "^1.1.7|^2",
+            "symfony/service-contracts": "^1.1|^2",
+            "symfony/var-exporter": "^4.2|^5.0"
+        },
+        "conflict": {
+            "doctrine/dbal": "<2.5",
+            "symfony/dependency-injection": "<3.4",
+            "symfony/http-kernel": "<4.4",
+            "symfony/var-dumper": "<4.4"
+        },
+        "provide": {
+            "psr/cache-implementation": "1.0",
+            "psr/simple-cache-implementation": "1.0",
+            "symfony/cache-implementation": "1.0"
+        },
+        "require-dev": {
+            "cache/integration-tests": "dev-master",
+            "doctrine/cache": "~1.6",
+            "doctrine/dbal": "~2.5",
+            "predis/predis": "~1.1",
+            "psr/simple-cache": "^1.0",
+            "symfony/config": "^4.2|^5.0",
+            "symfony/dependency-injection": "^3.4|^4.1|^5.0",
+            "symfony/var-dumper": "^4.4|^5.0"
+        },
+        "time": "2020-03-27T16:54:36+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "4.4-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\Cache\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony Cache component with PSR-6, PSR-16, and tags",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "caching",
+            "psr6"
+        ]
+    },
+    {
+        "name": "symfony/cache-contracts",
+        "version": "v2.0.1",
+        "version_normalized": "2.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/cache-contracts.git",
+            "reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
+            "reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7.2.5",
+            "psr/cache": "^1.0"
+        },
+        "suggest": {
+            "symfony/cache-implementation": ""
+        },
+        "time": "2019-11-18T17:27:11+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "2.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Contracts\\Cache\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Generic abstractions related to caching",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "abstractions",
+            "contracts",
+            "decoupling",
+            "interfaces",
+            "interoperability",
+            "standards"
+        ]
+    },
+    {
+        "name": "symfony/expression-language",
+        "version": "v3.4.39",
+        "version_normalized": "3.4.39.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/expression-language.git",
+            "reference": "206165f46c660f3231df0afbdeec6a62f81afc59"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/expression-language/zipball/206165f46c660f3231df0afbdeec6a62f81afc59",
+            "reference": "206165f46c660f3231df0afbdeec6a62f81afc59",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^5.5.9|>=7.0.8",
+            "symfony/cache": "~3.1|~4.0",
+            "symfony/polyfill-php70": "~1.6"
+        },
+        "time": "2020-03-16T08:31:04+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.4-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\ExpressionLanguage\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Fabien Potencier",
+                "email": "fabien@symfony.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony ExpressionLanguage Component",
+        "homepage": "https://symfony.com"
+    },
+    {
+        "name": "symfony/polyfill-php70",
+        "version": "v1.15.0",
+        "version_normalized": "1.15.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-php70.git",
+            "reference": "2a18e37a489803559284416df58c71ccebe50bf0"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0",
+            "reference": "2a18e37a489803559284416df58c71ccebe50bf0",
+            "shasum": ""
+        },
+        "require": {
+            "paragonie/random_compat": "~1.0|~2.0|~9.99",
+            "php": ">=5.3.3"
+        },
+        "time": "2020-02-27T09:26:54+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.15-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Php70\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ],
+            "classmap": [
+                "Resources/stubs"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/service-contracts",
+        "version": "v2.0.1",
+        "version_normalized": "2.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/service-contracts.git",
+            "reference": "144c5e51266b281231e947b51223ba14acf1a749"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
+            "reference": "144c5e51266b281231e947b51223ba14acf1a749",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7.2.5",
+            "psr/container": "^1.0"
+        },
+        "suggest": {
+            "symfony/service-implementation": ""
+        },
+        "time": "2019-11-18T17:27:11+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "2.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Contracts\\Service\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Generic abstractions related to writing services",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "abstractions",
+            "contracts",
+            "decoupling",
+            "interfaces",
+            "interoperability",
+            "standards"
+        ]
+    },
+    {
+        "name": "symfony/var-exporter",
+        "version": "v5.0.7",
+        "version_normalized": "5.0.7.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/var-exporter.git",
+            "reference": "ffd29a70370e466343e33154b5df197a07a13afa"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ffd29a70370e466343e33154b5df197a07a13afa",
+            "reference": "ffd29a70370e466343e33154b5df197a07a13afa",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7.2.5"
+        },
+        "require-dev": {
+            "symfony/var-dumper": "^4.4|^5.0"
+        },
+        "time": "2020-03-27T16:56:45+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "5.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Component\\VarExporter\\": ""
+            },
+            "exclude-from-classmap": [
+                "/Tests/"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "clone",
+            "construct",
+            "export",
+            "hydrate",
+            "instantiate",
+            "serialize"
+        ]
+    }
+]

+ 22 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Paragon Initiative Enterprises
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 5 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/build-phar.sh

@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+basedir=$( dirname $( readlink -f ${BASH_SOURCE[0]} ) )
+
+php -dphar.readonly=0 "$basedir/other/build_phar.php" $*

+ 34 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/composer.json

@@ -0,0 +1,34 @@
+{
+  "name":         "paragonie/random_compat",
+  "description":  "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+  "keywords": [
+    "csprng",
+    "random",
+    "polyfill",
+    "pseudorandom"
+  ],
+  "license":      "MIT",
+  "type":         "library",
+  "authors": [
+    {
+      "name":     "Paragon Initiative Enterprises",
+      "email":    "security@paragonie.com",
+      "homepage": "https://paragonie.com"
+    }
+  ],
+  "support": {
+    "issues":     "https://github.com/paragonie/random_compat/issues",
+    "email":      "info@paragonie.com",
+    "source":     "https://github.com/paragonie/random_compat"
+  },
+  "require": {
+    "php": "^7"
+  },
+  "require-dev": {
+    "vimeo/psalm": "^1",
+    "phpunit/phpunit": "4.*|5.*"
+  },
+  "suggest": {
+    "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+  }
+}

+ 5 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey

@@ -0,0 +1,5 @@
+-----BEGIN PUBLIC KEY-----
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEd+wCqJDrx5B4OldM0dQE0ZMX+lx1ZWm
+pui0SUqD4G29L3NGsz9UhJ/0HjBdbnkhIK5xviT0X5vtjacF6ajgcCArbTB+ds+p
++h7Q084NuSuIpNb6YPfoUFgC/CL9kAoc
+-----END PUBLIC KEY-----

+ 11 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc

@@ -0,0 +1,11 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.22 (MingW32)
+
+iQEcBAABAgAGBQJWtW1hAAoJEGuXocKCZATaJf0H+wbZGgskK1dcRTsuVJl9IWip
+QwGw/qIKI280SD6/ckoUMxKDCJiFuPR14zmqnS36k7N5UNPnpdTJTS8T11jttSpg
+1LCmgpbEIpgaTah+cELDqFCav99fS+bEiAL5lWDAHBTE/XPjGVCqeehyPYref4IW
+NDBIEsvnHPHPLsn6X5jq4+Yj5oUixgxaMPiR+bcO4Sh+RzOVB6i2D0upWfRXBFXA
+NNnsg9/zjvoC7ZW73y9uSH+dPJTt/Vgfeiv52/v41XliyzbUyLalf02GNPY+9goV
+JHG1ulEEBJOCiUD9cE1PUIJwHA/HqyhHIvV350YoEFiHl8iSwm7SiZu5kPjaq74=
+=B6+8
+-----END PGP SIGNATURE-----

+ 32 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/lib/random.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Random_* Compatibility Library
+ * for using the new PHP 7 random_* API in PHP 5 projects
+ *
+ * @version 2.99.99
+ * @released 2018-06-06
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// NOP

+ 57 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/other/build_phar.php

@@ -0,0 +1,57 @@
+<?php
+$dist = dirname(__DIR__).'/dist';
+if (!is_dir($dist)) {
+    mkdir($dist, 0755);
+}
+if (file_exists($dist.'/random_compat.phar')) {
+    unlink($dist.'/random_compat.phar');
+}
+$phar = new Phar(
+    $dist.'/random_compat.phar',
+    FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME,
+    'random_compat.phar'
+);
+rename(
+    dirname(__DIR__).'/lib/random.php', 
+    dirname(__DIR__).'/lib/index.php'
+);
+$phar->buildFromDirectory(dirname(__DIR__).'/lib');
+rename(
+    dirname(__DIR__).'/lib/index.php', 
+    dirname(__DIR__).'/lib/random.php'
+);
+
+/**
+ * If we pass an (optional) path to a private key as a second argument, we will
+ * sign the Phar with OpenSSL.
+ * 
+ * If you leave this out, it will produce an unsigned .phar!
+ */
+if ($argc > 1) {
+    if (!@is_readable($argv[1])) {
+        echo 'Could not read the private key file:', $argv[1], "\n";
+        exit(255);
+    }
+    $pkeyFile = file_get_contents($argv[1]);
+    
+    $private = openssl_get_privatekey($pkeyFile);
+    if ($private !== false) {
+        $pkey = '';
+        openssl_pkey_export($private, $pkey);
+        $phar->setSignatureAlgorithm(Phar::OPENSSL, $pkey);
+        
+        /**
+         * Save the corresponding public key to the file
+         */
+        if (!@is_readable($dist.'/random_compat.phar.pubkey')) {
+            $details = openssl_pkey_get_details($private);
+            file_put_contents(
+                $dist.'/random_compat.phar.pubkey',
+                $details['key']
+            );
+        }
+    } else {
+        echo 'An error occurred reading the private key from OpenSSL.', "\n";
+        exit(255);
+    }
+}

+ 9 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/psalm-autoload.php

@@ -0,0 +1,9 @@
+<?php
+
+require_once 'lib/byte_safe_strings.php';
+require_once 'lib/cast_to_int.php';
+require_once 'lib/error_polyfill.php';
+require_once 'other/ide_stubs/libsodium.php';
+require_once 'lib/random.php';
+
+$int = random_int(0, 65536);

+ 19 - 0
user/plugins/admin-addon-user-manager/vendor/paragonie/random_compat/psalm.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<psalm
+    autoloader="psalm-autoload.php"
+    stopOnFirstError="false"
+    useDocblockTypes="true"
+>
+    <projectFiles>
+        <directory name="lib" />
+    </projectFiles>
+    <issueHandlers>
+        <RedundantConditionGivenDocblockType errorLevel="info" />
+        <UnresolvableInclude errorLevel="info" />
+        <DuplicateClass errorLevel="info" />
+        <InvalidOperand errorLevel="info" />
+        <UndefinedConstant errorLevel="info" />
+        <MissingReturnType errorLevel="info" />
+        <InvalidReturnType errorLevel="info" />
+    </issueHandlers>
+</psalm>

+ 16 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/CHANGELOG.md

@@ -0,0 +1,16 @@
+# Changelog
+
+All notable changes to this project will be documented in this file, in reverse chronological order by release.
+
+## 1.0.1 - 2016-08-06
+
+### Fixed
+
+- Make spacing consistent in phpdoc annotations php-fig/cache#9 - chalasr
+- Fix grammar in phpdoc annotations php-fig/cache#10 - chalasr
+- Be more specific in docblocks that `getItems()` and `deleteItems()` take an array of strings (`string[]`) compared to just `array` php-fig/cache#8 - GrahamCampbell
+- For `expiresAt()` and `expiresAfter()` in CacheItemInterface fix docblock to specify null as a valid parameters as well as an implementation of DateTimeInterface php-fig/cache#7 - GrahamCampbell
+
+## 1.0.0 - 2015-12-11
+
+Initial stable release; reflects accepted PSR-6 specification

+ 19 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/LICENSE.txt

@@ -0,0 +1,19 @@
+Copyright (c) 2015 PHP Framework Interoperability Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 9 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/README.md

@@ -0,0 +1,9 @@
+PSR Cache
+=========
+
+This repository holds all interfaces defined by
+[PSR-6](http://www.php-fig.org/psr/psr-6/).
+
+Note that this is not a Cache implementation of its own. It is merely an
+interface that describes a Cache implementation. See the specification for more 
+details.

+ 25 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/composer.json

@@ -0,0 +1,25 @@
+{
+    "name": "psr/cache",
+    "description": "Common interface for caching libraries",
+    "keywords": ["psr", "psr-6", "cache"],
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "http://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Cache\\": "src/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.0.x-dev"
+        }
+    }
+}

+ 10 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheException.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Psr\Cache;
+
+/**
+ * Exception interface for all exceptions thrown by an Implementing Library.
+ */
+interface CacheException
+{
+}

+ 105 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheItemInterface.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace Psr\Cache;
+
+/**
+ * CacheItemInterface defines an interface for interacting with objects inside a cache.
+ *
+ * Each Item object MUST be associated with a specific key, which can be set
+ * according to the implementing system and is typically passed by the
+ * Cache\CacheItemPoolInterface object.
+ *
+ * The Cache\CacheItemInterface object encapsulates the storage and retrieval of
+ * cache items. Each Cache\CacheItemInterface is generated by a
+ * Cache\CacheItemPoolInterface object, which is responsible for any required
+ * setup as well as associating the object with a unique Key.
+ * Cache\CacheItemInterface objects MUST be able to store and retrieve any type
+ * of PHP value defined in the Data section of the specification.
+ *
+ * Calling Libraries MUST NOT instantiate Item objects themselves. They may only
+ * be requested from a Pool object via the getItem() method.  Calling Libraries
+ * SHOULD NOT assume that an Item created by one Implementing Library is
+ * compatible with a Pool from another Implementing Library.
+ */
+interface CacheItemInterface
+{
+    /**
+     * Returns the key for the current cache item.
+     *
+     * The key is loaded by the Implementing Library, but should be available to
+     * the higher level callers when needed.
+     *
+     * @return string
+     *   The key string for this cache item.
+     */
+    public function getKey();
+
+    /**
+     * Retrieves the value of the item from the cache associated with this object's key.
+     *
+     * The value returned must be identical to the value originally stored by set().
+     *
+     * If isHit() returns false, this method MUST return null. Note that null
+     * is a legitimate cached value, so the isHit() method SHOULD be used to
+     * differentiate between "null value was found" and "no value was found."
+     *
+     * @return mixed
+     *   The value corresponding to this cache item's key, or null if not found.
+     */
+    public function get();
+
+    /**
+     * Confirms if the cache item lookup resulted in a cache hit.
+     *
+     * Note: This method MUST NOT have a race condition between calling isHit()
+     * and calling get().
+     *
+     * @return bool
+     *   True if the request resulted in a cache hit. False otherwise.
+     */
+    public function isHit();
+
+    /**
+     * Sets the value represented by this cache item.
+     *
+     * The $value argument may be any item that can be serialized by PHP,
+     * although the method of serialization is left up to the Implementing
+     * Library.
+     *
+     * @param mixed $value
+     *   The serializable value to be stored.
+     *
+     * @return static
+     *   The invoked object.
+     */
+    public function set($value);
+
+    /**
+     * Sets the expiration time for this cache item.
+     *
+     * @param \DateTimeInterface|null $expiration
+     *   The point in time after which the item MUST be considered expired.
+     *   If null is passed explicitly, a default value MAY be used. If none is set,
+     *   the value should be stored permanently or for as long as the
+     *   implementation allows.
+     *
+     * @return static
+     *   The called object.
+     */
+    public function expiresAt($expiration);
+
+    /**
+     * Sets the expiration time for this cache item.
+     *
+     * @param int|\DateInterval|null $time
+     *   The period of time from the present after which the item MUST be considered
+     *   expired. An integer parameter is understood to be the time in seconds until
+     *   expiration. If null is passed explicitly, a default value MAY be used.
+     *   If none is set, the value should be stored permanently or for as long as the
+     *   implementation allows.
+     *
+     * @return static
+     *   The called object.
+     */
+    public function expiresAfter($time);
+}

+ 138 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/src/CacheItemPoolInterface.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Psr\Cache;
+
+/**
+ * CacheItemPoolInterface generates CacheItemInterface objects.
+ *
+ * The primary purpose of Cache\CacheItemPoolInterface is to accept a key from
+ * the Calling Library and return the associated Cache\CacheItemInterface object.
+ * It is also the primary point of interaction with the entire cache collection.
+ * All configuration and initialization of the Pool is left up to an
+ * Implementing Library.
+ */
+interface CacheItemPoolInterface
+{
+    /**
+     * Returns a Cache Item representing the specified key.
+     *
+     * This method must always return a CacheItemInterface object, even in case of
+     * a cache miss. It MUST NOT return null.
+     *
+     * @param string $key
+     *   The key for which to return the corresponding Cache Item.
+     *
+     * @throws InvalidArgumentException
+     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
+     *   MUST be thrown.
+     *
+     * @return CacheItemInterface
+     *   The corresponding Cache Item.
+     */
+    public function getItem($key);
+
+    /**
+     * Returns a traversable set of cache items.
+     *
+     * @param string[] $keys
+     *   An indexed array of keys of items to retrieve.
+     *
+     * @throws InvalidArgumentException
+     *   If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
+     *   MUST be thrown.
+     *
+     * @return array|\Traversable
+     *   A traversable collection of Cache Items keyed by the cache keys of
+     *   each item. A Cache item will be returned for each key, even if that
+     *   key is not found. However, if no keys are specified then an empty
+     *   traversable MUST be returned instead.
+     */
+    public function getItems(array $keys = array());
+
+    /**
+     * Confirms if the cache contains specified cache item.
+     *
+     * Note: This method MAY avoid retrieving the cached value for performance reasons.
+     * This could result in a race condition with CacheItemInterface::get(). To avoid
+     * such situation use CacheItemInterface::isHit() instead.
+     *
+     * @param string $key
+     *   The key for which to check existence.
+     *
+     * @throws InvalidArgumentException
+     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
+     *   MUST be thrown.
+     *
+     * @return bool
+     *   True if item exists in the cache, false otherwise.
+     */
+    public function hasItem($key);
+
+    /**
+     * Deletes all items in the pool.
+     *
+     * @return bool
+     *   True if the pool was successfully cleared. False if there was an error.
+     */
+    public function clear();
+
+    /**
+     * Removes the item from the pool.
+     *
+     * @param string $key
+     *   The key to delete.
+     *
+     * @throws InvalidArgumentException
+     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
+     *   MUST be thrown.
+     *
+     * @return bool
+     *   True if the item was successfully removed. False if there was an error.
+     */
+    public function deleteItem($key);
+
+    /**
+     * Removes multiple items from the pool.
+     *
+     * @param string[] $keys
+     *   An array of keys that should be removed from the pool.
+
+     * @throws InvalidArgumentException
+     *   If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
+     *   MUST be thrown.
+     *
+     * @return bool
+     *   True if the items were successfully removed. False if there was an error.
+     */
+    public function deleteItems(array $keys);
+
+    /**
+     * Persists a cache item immediately.
+     *
+     * @param CacheItemInterface $item
+     *   The cache item to save.
+     *
+     * @return bool
+     *   True if the item was successfully persisted. False if there was an error.
+     */
+    public function save(CacheItemInterface $item);
+
+    /**
+     * Sets a cache item to be persisted later.
+     *
+     * @param CacheItemInterface $item
+     *   The cache item to save.
+     *
+     * @return bool
+     *   False if the item could not be queued or if a commit was attempted and failed. True otherwise.
+     */
+    public function saveDeferred(CacheItemInterface $item);
+
+    /**
+     * Persists any deferred cache items.
+     *
+     * @return bool
+     *   True if all not-yet-saved items were successfully saved or there were none. False otherwise.
+     */
+    public function commit();
+}

+ 13 - 0
user/plugins/admin-addon-user-manager/vendor/psr/cache/src/InvalidArgumentException.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace Psr\Cache;
+
+/**
+ * Exception interface for invalid cache arguments.
+ *
+ * Any time an invalid argument is passed into a method it must throw an
+ * exception class which implements Psr\Cache\InvalidArgumentException.
+ */
+interface InvalidArgumentException extends CacheException
+{
+}

+ 3 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/.gitignore

@@ -0,0 +1,3 @@
+composer.lock
+composer.phar
+/vendor/

+ 21 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2016 container-interop
+Copyright (c) 2016 PHP Framework Interoperability Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 5 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/README.md

@@ -0,0 +1,5 @@
+# PSR Container
+
+This repository holds all interfaces/classes/traits related to [PSR-11](https://github.com/container-interop/fig-standards/blob/master/proposed/container.md).
+
+Note that this is not a container implementation of its own. See the specification for more details.

+ 27 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/composer.json

@@ -0,0 +1,27 @@
+{
+    "name": "psr/container",
+    "type": "library",
+    "description": "Common Container Interface (PHP FIG PSR-11)",
+    "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"],
+    "homepage": "https://github.com/php-fig/container",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "http://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Container\\": "src/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.0.x-dev"
+        }
+    }
+}

+ 13 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/src/ContainerExceptionInterface.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
+ */
+
+namespace Psr\Container;
+
+/**
+ * Base interface representing a generic exception in a container.
+ */
+interface ContainerExceptionInterface
+{
+}

+ 37 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/src/ContainerInterface.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
+ */
+
+namespace Psr\Container;
+
+/**
+ * Describes the interface of a container that exposes methods to read its entries.
+ */
+interface ContainerInterface
+{
+    /**
+     * Finds an entry of the container by its identifier and returns it.
+     *
+     * @param string $id Identifier of the entry to look for.
+     *
+     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
+     * @throws ContainerExceptionInterface Error while retrieving the entry.
+     *
+     * @return mixed Entry.
+     */
+    public function get($id);
+
+    /**
+     * Returns true if the container can return an entry for the given identifier.
+     * Returns false otherwise.
+     *
+     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
+     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
+     *
+     * @param string $id Identifier of the entry to look for.
+     *
+     * @return bool
+     */
+    public function has($id);
+}

+ 13 - 0
user/plugins/admin-addon-user-manager/vendor/psr/container/src/NotFoundExceptionInterface.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
+ */
+
+namespace Psr\Container;
+
+/**
+ * No entry was found in the container.
+ */
+interface NotFoundExceptionInterface extends ContainerExceptionInterface
+{
+}

+ 19 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2012 PHP Framework Interoperability Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy 
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights 
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
+copies of the Software, and to permit persons to whom the Software is 
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in 
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 128 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/AbstractLogger.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class AbstractLogger implements LoggerInterface
+{
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function emergency($message, array $context = array())
+    {
+        $this->log(LogLevel::EMERGENCY, $message, $context);
+    }
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function alert($message, array $context = array())
+    {
+        $this->log(LogLevel::ALERT, $message, $context);
+    }
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function critical($message, array $context = array())
+    {
+        $this->log(LogLevel::CRITICAL, $message, $context);
+    }
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function error($message, array $context = array())
+    {
+        $this->log(LogLevel::ERROR, $message, $context);
+    }
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function warning($message, array $context = array())
+    {
+        $this->log(LogLevel::WARNING, $message, $context);
+    }
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function notice($message, array $context = array())
+    {
+        $this->log(LogLevel::NOTICE, $message, $context);
+    }
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function info($message, array $context = array())
+    {
+        $this->log(LogLevel::INFO, $message, $context);
+    }
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function debug($message, array $context = array())
+    {
+        $this->log(LogLevel::DEBUG, $message, $context);
+    }
+}

+ 7 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/InvalidArgumentException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Psr\Log;
+
+class InvalidArgumentException extends \InvalidArgumentException
+{
+}

+ 18 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LogLevel.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes log levels.
+ */
+class LogLevel
+{
+    const EMERGENCY = 'emergency';
+    const ALERT     = 'alert';
+    const CRITICAL  = 'critical';
+    const ERROR     = 'error';
+    const WARNING   = 'warning';
+    const NOTICE    = 'notice';
+    const INFO      = 'info';
+    const DEBUG     = 'debug';
+}

+ 18 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerAwareInterface.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes a logger-aware instance.
+ */
+interface LoggerAwareInterface
+{
+    /**
+     * Sets a logger instance on the object.
+     *
+     * @param LoggerInterface $logger
+     *
+     * @return void
+     */
+    public function setLogger(LoggerInterface $logger);
+}

+ 26 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerAwareTrait.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Basic Implementation of LoggerAwareInterface.
+ */
+trait LoggerAwareTrait
+{
+    /**
+     * The logger instance.
+     *
+     * @var LoggerInterface
+     */
+    protected $logger;
+
+    /**
+     * Sets a logger.
+     *
+     * @param LoggerInterface $logger
+     */
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+}

+ 125 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerInterface.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes a logger instance.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data. The only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface LoggerInterface
+{
+    /**
+     * System is unusable.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function emergency($message, array $context = array());
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function alert($message, array $context = array());
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function critical($message, array $context = array());
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function error($message, array $context = array());
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function warning($message, array $context = array());
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function notice($message, array $context = array());
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function info($message, array $context = array());
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     */
+    public function debug($message, array $context = array());
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed   $level
+     * @param string  $message
+     * @param mixed[] $context
+     *
+     * @return void
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    public function log($level, $message, array $context = array());
+}

+ 142 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/LoggerTrait.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This is a simple Logger trait that classes unable to extend AbstractLogger
+ * (because they extend another class, etc) can include.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+trait LoggerTrait
+{
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function emergency($message, array $context = array())
+    {
+        $this->log(LogLevel::EMERGENCY, $message, $context);
+    }
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function alert($message, array $context = array())
+    {
+        $this->log(LogLevel::ALERT, $message, $context);
+    }
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function critical($message, array $context = array())
+    {
+        $this->log(LogLevel::CRITICAL, $message, $context);
+    }
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function error($message, array $context = array())
+    {
+        $this->log(LogLevel::ERROR, $message, $context);
+    }
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function warning($message, array $context = array())
+    {
+        $this->log(LogLevel::WARNING, $message, $context);
+    }
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function notice($message, array $context = array())
+    {
+        $this->log(LogLevel::NOTICE, $message, $context);
+    }
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function info($message, array $context = array())
+    {
+        $this->log(LogLevel::INFO, $message, $context);
+    }
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     */
+    public function debug($message, array $context = array())
+    {
+        $this->log(LogLevel::DEBUG, $message, $context);
+    }
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    abstract public function log($level, $message, array $context = array());
+}

+ 30 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/NullLogger.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This Logger can be used to avoid conditional log calls.
+ *
+ * Logging should always be optional, and if no logger is provided to your
+ * library creating a NullLogger instance to have something to throw logs at
+ * is a good way to avoid littering your code with `if ($this->logger) { }`
+ * blocks.
+ */
+class NullLogger extends AbstractLogger
+{
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    public function log($level, $message, array $context = array())
+    {
+        // noop
+    }
+}

+ 18 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/DummyTest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Log\Test;
+
+/**
+ * This class is internal and does not follow the BC promise.
+ *
+ * Do NOT use this class in any way.
+ *
+ * @internal
+ */
+class DummyTest
+{
+    public function __toString()
+    {
+        return 'DummyTest';
+    }
+}

+ 138 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Psr\Log\Test;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Provides a base test class for ensuring compliance with the LoggerInterface.
+ *
+ * Implementors can extend the class and implement abstract methods to run this
+ * as part of their test suite.
+ */
+abstract class LoggerInterfaceTest extends TestCase
+{
+    /**
+     * @return LoggerInterface
+     */
+    abstract public function getLogger();
+
+    /**
+     * This must return the log messages in order.
+     *
+     * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>".
+     *
+     * Example ->error('Foo') would yield "error Foo".
+     *
+     * @return string[]
+     */
+    abstract public function getLogs();
+
+    public function testImplements()
+    {
+        $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
+    }
+
+    /**
+     * @dataProvider provideLevelsAndMessages
+     */
+    public function testLogsAtAllLevels($level, $message)
+    {
+        $logger = $this->getLogger();
+        $logger->{$level}($message, array('user' => 'Bob'));
+        $logger->log($level, $message, array('user' => 'Bob'));
+
+        $expected = array(
+            $level.' message of level '.$level.' with context: Bob',
+            $level.' message of level '.$level.' with context: Bob',
+        );
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function provideLevelsAndMessages()
+    {
+        return array(
+            LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
+            LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
+            LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
+            LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
+            LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
+            LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
+            LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
+            LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
+        );
+    }
+
+    /**
+     * @expectedException \Psr\Log\InvalidArgumentException
+     */
+    public function testThrowsOnInvalidLevel()
+    {
+        $logger = $this->getLogger();
+        $logger->log('invalid level', 'Foo');
+    }
+
+    public function testContextReplacement()
+    {
+        $logger = $this->getLogger();
+        $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
+
+        $expected = array('info {Message {nothing} Bob Bar a}');
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testObjectCastToString()
+    {
+        if (method_exists($this, 'createPartialMock')) {
+            $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
+        } else {
+            $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
+        }
+        $dummy->expects($this->once())
+            ->method('__toString')
+            ->will($this->returnValue('DUMMY'));
+
+        $this->getLogger()->warning($dummy);
+
+        $expected = array('warning DUMMY');
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testContextCanContainAnything()
+    {
+        $closed = fopen('php://memory', 'r');
+        fclose($closed);
+
+        $context = array(
+            'bool' => true,
+            'null' => null,
+            'string' => 'Foo',
+            'int' => 0,
+            'float' => 0.5,
+            'nested' => array('with object' => new DummyTest),
+            'object' => new \DateTime,
+            'resource' => fopen('php://memory', 'r'),
+            'closed' => $closed,
+        );
+
+        $this->getLogger()->warning('Crazy context data', $context);
+
+        $expected = array('warning Crazy context data');
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testContextExceptionKeyCanBeExceptionOrOtherValues()
+    {
+        $logger = $this->getLogger();
+        $logger->warning('Random message', array('exception' => 'oops'));
+        $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
+
+        $expected = array(
+            'warning Random message',
+            'critical Uncaught Exception!'
+        );
+        $this->assertEquals($expected, $this->getLogs());
+    }
+}

+ 147 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/Psr/Log/Test/TestLogger.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace Psr\Log\Test;
+
+use Psr\Log\AbstractLogger;
+
+/**
+ * Used for testing purposes.
+ *
+ * It records all records and gives you access to them for verification.
+ *
+ * @method bool hasEmergency($record)
+ * @method bool hasAlert($record)
+ * @method bool hasCritical($record)
+ * @method bool hasError($record)
+ * @method bool hasWarning($record)
+ * @method bool hasNotice($record)
+ * @method bool hasInfo($record)
+ * @method bool hasDebug($record)
+ *
+ * @method bool hasEmergencyRecords()
+ * @method bool hasAlertRecords()
+ * @method bool hasCriticalRecords()
+ * @method bool hasErrorRecords()
+ * @method bool hasWarningRecords()
+ * @method bool hasNoticeRecords()
+ * @method bool hasInfoRecords()
+ * @method bool hasDebugRecords()
+ *
+ * @method bool hasEmergencyThatContains($message)
+ * @method bool hasAlertThatContains($message)
+ * @method bool hasCriticalThatContains($message)
+ * @method bool hasErrorThatContains($message)
+ * @method bool hasWarningThatContains($message)
+ * @method bool hasNoticeThatContains($message)
+ * @method bool hasInfoThatContains($message)
+ * @method bool hasDebugThatContains($message)
+ *
+ * @method bool hasEmergencyThatMatches($message)
+ * @method bool hasAlertThatMatches($message)
+ * @method bool hasCriticalThatMatches($message)
+ * @method bool hasErrorThatMatches($message)
+ * @method bool hasWarningThatMatches($message)
+ * @method bool hasNoticeThatMatches($message)
+ * @method bool hasInfoThatMatches($message)
+ * @method bool hasDebugThatMatches($message)
+ *
+ * @method bool hasEmergencyThatPasses($message)
+ * @method bool hasAlertThatPasses($message)
+ * @method bool hasCriticalThatPasses($message)
+ * @method bool hasErrorThatPasses($message)
+ * @method bool hasWarningThatPasses($message)
+ * @method bool hasNoticeThatPasses($message)
+ * @method bool hasInfoThatPasses($message)
+ * @method bool hasDebugThatPasses($message)
+ */
+class TestLogger extends AbstractLogger
+{
+    /**
+     * @var array
+     */
+    public $records = [];
+
+    public $recordsByLevel = [];
+
+    /**
+     * @inheritdoc
+     */
+    public function log($level, $message, array $context = [])
+    {
+        $record = [
+            'level' => $level,
+            'message' => $message,
+            'context' => $context,
+        ];
+
+        $this->recordsByLevel[$record['level']][] = $record;
+        $this->records[] = $record;
+    }
+
+    public function hasRecords($level)
+    {
+        return isset($this->recordsByLevel[$level]);
+    }
+
+    public function hasRecord($record, $level)
+    {
+        if (is_string($record)) {
+            $record = ['message' => $record];
+        }
+        return $this->hasRecordThatPasses(function ($rec) use ($record) {
+            if ($rec['message'] !== $record['message']) {
+                return false;
+            }
+            if (isset($record['context']) && $rec['context'] !== $record['context']) {
+                return false;
+            }
+            return true;
+        }, $level);
+    }
+
+    public function hasRecordThatContains($message, $level)
+    {
+        return $this->hasRecordThatPasses(function ($rec) use ($message) {
+            return strpos($rec['message'], $message) !== false;
+        }, $level);
+    }
+
+    public function hasRecordThatMatches($regex, $level)
+    {
+        return $this->hasRecordThatPasses(function ($rec) use ($regex) {
+            return preg_match($regex, $rec['message']) > 0;
+        }, $level);
+    }
+
+    public function hasRecordThatPasses(callable $predicate, $level)
+    {
+        if (!isset($this->recordsByLevel[$level])) {
+            return false;
+        }
+        foreach ($this->recordsByLevel[$level] as $i => $rec) {
+            if (call_user_func($predicate, $rec, $i)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public function __call($method, $args)
+    {
+        if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
+            $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
+            $level = strtolower($matches[2]);
+            if (method_exists($this, $genericMethod)) {
+                $args[] = $level;
+                return call_user_func_array([$this, $genericMethod], $args);
+            }
+        }
+        throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
+    }
+
+    public function reset()
+    {
+        $this->records = [];
+        $this->recordsByLevel = [];
+    }
+}

+ 58 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/README.md

@@ -0,0 +1,58 @@
+PSR Log
+=======
+
+This repository holds all interfaces/classes/traits related to
+[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
+
+Note that this is not a logger of its own. It is merely an interface that
+describes a logger. See the specification for more details.
+
+Installation
+------------
+
+```bash
+composer require psr/log
+```
+
+Usage
+-----
+
+If you need a logger, you can use the interface like this:
+
+```php
+<?php
+
+use Psr\Log\LoggerInterface;
+
+class Foo
+{
+    private $logger;
+
+    public function __construct(LoggerInterface $logger = null)
+    {
+        $this->logger = $logger;
+    }
+
+    public function doSomething()
+    {
+        if ($this->logger) {
+            $this->logger->info('Doing work');
+        }
+           
+        try {
+            $this->doSomethingElse();
+        } catch (Exception $exception) {
+            $this->logger->error('Oh no!', array('exception' => $exception));
+        }
+
+        // do something useful
+    }
+}
+```
+
+You can then pick one of the implementations of the interface to get a logger.
+
+If you want to implement the interface, you can require this package and
+implement `Psr\Log\LoggerInterface` in your code. Please read the
+[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+for details.

+ 26 - 0
user/plugins/admin-addon-user-manager/vendor/psr/log/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "psr/log",
+    "description": "Common interface for logging libraries",
+    "keywords": ["psr", "psr-3", "log"],
+    "homepage": "https://github.com/php-fig/log",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "http://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Log\\": "Psr/Log/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.1.x-dev"
+        }
+    }
+}

+ 3 - 0
user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/.gitignore

@@ -0,0 +1,3 @@
+vendor/
+composer.lock
+phpunit.xml

+ 57 - 0
user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CacheInterface.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Cache;
+
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\InvalidArgumentException;
+
+/**
+ * Covers most simple to advanced caching needs.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+interface CacheInterface
+{
+    /**
+     * Fetches a value from the pool or computes it if not found.
+     *
+     * On cache misses, a callback is called that should return the missing value.
+     * This callback is given a PSR-6 CacheItemInterface instance corresponding to the
+     * requested key, that could be used e.g. for expiration control. It could also
+     * be an ItemInterface instance when its additional features are needed.
+     *
+     * @param string                     $key       The key of the item to retrieve from the cache
+     * @param callable|CallbackInterface $callback  Should return the computed value for the given key/item
+     * @param float|null                 $beta      A float that, as it grows, controls the likeliness of triggering
+     *                                              early expiration. 0 disables it, INF forces immediate expiration.
+     *                                              The default (or providing null) is implementation dependent but should
+     *                                              typically be 1.0, which should provide optimal stampede protection.
+     *                                              See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
+     * @param array                      &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()}
+     *
+     * @return mixed The value corresponding to the provided key
+     *
+     * @throws InvalidArgumentException When $key is not valid or when $beta is negative
+     */
+    public function get(string $key, callable $callback, float $beta = null, array &$metadata = null);
+
+    /**
+     * Removes an item from the pool.
+     *
+     * @param string $key The key to delete
+     *
+     * @throws InvalidArgumentException When $key is not valid
+     *
+     * @return bool True if the item was successfully removed, false if there was any error
+     */
+    public function delete(string $key): bool;
+}

+ 76 - 0
user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CacheTrait.php

@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Cache;
+
+use Psr\Cache\CacheItemPoolInterface;
+use Psr\Cache\InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+trait CacheTrait
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
+    {
+        return $this->doGet($this, $key, $callback, $beta, $metadata);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function delete(string $key): bool
+    {
+        return $this->deleteItem($key);
+    }
+
+    private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null, LoggerInterface $logger = null)
+    {
+        if (0 > $beta = $beta ?? 1.0) {
+            throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', \get_class($this), $beta)) extends \InvalidArgumentException implements InvalidArgumentException {
+            };
+        }
+
+        $item = $pool->getItem($key);
+        $recompute = !$item->isHit() || INF === $beta;
+        $metadata = $item instanceof ItemInterface ? $item->getMetadata() : [];
+
+        if (!$recompute && $metadata) {
+            $expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false;
+            $ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false;
+
+            if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX)) {
+                // force applying defaultLifetime to expiry
+                $item->expiresAt(null);
+                $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
+                    'key' => $key,
+                    'delta' => sprintf('%.1f', $expiry - $now),
+                ]);
+            }
+        }
+
+        if ($recompute) {
+            $save = true;
+            $item->set($callback($item, $save));
+            if ($save) {
+                $pool->save($item);
+            }
+        }
+
+        return $item->get();
+    }
+}

+ 30 - 0
user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/CallbackInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Cache;
+
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * Computes and returns the cached value of an item.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+interface CallbackInterface
+{
+    /**
+     * @param CacheItemInterface|ItemInterface $item  The item to compute the value for
+     * @param bool                             &$save Should be set to false when the value should not be saved in the pool
+     *
+     * @return mixed The computed value for the passed item
+     */
+    public function __invoke(CacheItemInterface $item, bool &$save);
+}

+ 65 - 0
user/plugins/admin-addon-user-manager/vendor/symfony/cache-contracts/ItemInterface.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Cache;
+
+use Psr\Cache\CacheException;
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\InvalidArgumentException;
+
+/**
+ * Augments PSR-6's CacheItemInterface with support for tags and metadata.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+interface ItemInterface extends CacheItemInterface
+{
+    /**
+     * References the Unix timestamp stating when the item will expire.
+     */
+    const METADATA_EXPIRY = 'expiry';
+
+    /**
+     * References the time the item took to be created, in milliseconds.
+     */
+    const METADATA_CTIME = 'ctime';
+
+    /**
+     * References the list of tags that were assigned to the item, as string[].
+     */
+    const METADATA_TAGS = 'tags';
+
+    /**
+     * Reserved characters that cannot be used in a key or tag.
+     */
+    const RESERVED_CHARACTERS = '{}()/\@:';
+
+    /**
+     * Adds a tag to a cache item.
+     *
+     * Tags are strings that follow the same validation rules as keys.
+     *
+     * @param string|string[] $tags A tag or array of tags
+     *
+     * @return $this
+     *
+     * @throws InvalidArgumentException When $tag is not valid
+     * @throws CacheException           When the item comes from a pool that is not tag-aware
+     */
+    public function tag($tags): self;
+
+    /**
+     * Returns a list of metadata info that were saved alongside with the cached value.
+     *
+     * See ItemInterface::METADATA_* consts for keys potentially found in the returned array.
+     */
+    public function getMetadata(): array;
+}

Some files were not shown because too many files changed in this diff