summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2018-05-16 08:35:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2018-05-16 08:35:28 +0000
commit8ed11e87c4ab4dcd7f706186bb8ea26a6f32aefc (patch)
tree851cbaaaa8f0cb53680b9fb43a2688d8f0d2b726
parentInitial commit. (diff)
downloadmatomo-plugin-loginldap-8ed11e87c4ab4dcd7f706186bb8ea26a6f32aefc.zip
matomo-plugin-loginldap-8ed11e87c4ab4dcd7f706186bb8ea26a6f32aefc.tar.xz
Adding upstream version 4.0.4.upstream/4.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml133
-rw-r--r--API.php152
-rw-r--r--Auth/Base.php402
-rw-r--r--Auth/LdapAuth.php137
-rw-r--r--Auth/SynchronizedAuth.php150
-rw-r--r--Auth/WebServerAuth.php180
-rw-r--r--CHANGELOG.md172
-rw-r--r--Commands/SynchronizeUsers.php111
-rw-r--r--Config.php309
-rw-r--r--Controller.php82
-rw-r--r--Ldap/Client.php401
-rw-r--r--Ldap/Exceptions/ConnectionException.php18
-rw-r--r--Ldap/ServerInfo.php308
-rw-r--r--LdapInterop/UserAccessAttributeParser.php405
-rw-r--r--LdapInterop/UserAccessMapper.php318
-rw-r--r--LdapInterop/UserMapper.php480
-rw-r--r--LdapInterop/UserSynchronizer.php333
-rw-r--r--LoginLdap.php210
-rw-r--r--Menu.php21
-rw-r--r--Model/LdapUsers.php639
-rw-r--r--README.md245
-rw-r--r--Updates/3.0.0.php27
-rw-r--r--angularjs/admin/admin.controller.js82
-rw-r--r--angularjs/admin/admin.controller.less90
-rw-r--r--angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.html26
-rw-r--r--angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.js103
-rw-r--r--angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.less10
-rw-r--r--composer.json29
-rw-r--r--lang/ar.json21
-rw-r--r--lang/be.json5
-rw-r--r--lang/bg.json6
-rw-r--r--lang/bn.json5
-rw-r--r--lang/ca.json5
-rw-r--r--lang/cs.json91
-rw-r--r--lang/da.json85
-rw-r--r--lang/de.json91
-rw-r--r--lang/el.json91
-rw-r--r--lang/en.json91
-rw-r--r--lang/es.json91
-rw-r--r--lang/et.json33
-rw-r--r--lang/eu.json5
-rw-r--r--lang/fa.json5
-rw-r--r--lang/fi.json41
-rw-r--r--lang/fr.json91
-rw-r--r--lang/he.json5
-rw-r--r--lang/hi.json30
-rw-r--r--lang/hu.json5
-rw-r--r--lang/id.json15
-rw-r--r--lang/it.json91
-rw-r--r--lang/ja.json91
-rw-r--r--lang/ka.json5
-rw-r--r--lang/ko.json5
-rw-r--r--lang/lt.json11
-rw-r--r--lang/lv.json5
-rw-r--r--lang/nb.json18
-rw-r--r--lang/nl.json76
-rw-r--r--lang/nn.json5
-rw-r--r--lang/pl.json91
-rw-r--r--lang/pt-br.json89
-rw-r--r--lang/pt.json6
-rw-r--r--lang/ro.json5
-rw-r--r--lang/ru.json54
-rw-r--r--lang/sk.json6
-rw-r--r--lang/sl.json5
-rw-r--r--lang/sq.json91
-rw-r--r--lang/sr.json91
-rw-r--r--lang/sv.json91
-rw-r--r--lang/th.json5
-rw-r--r--lang/tl.json21
-rw-r--r--lang/tr.json91
-rw-r--r--lang/uk.json5
-rw-r--r--lang/vi.json6
-rw-r--r--lang/zh-cn.json13
-rw-r--r--lang/zh-tw.json91
-rw-r--r--plugin.json30
-rw-r--r--screenshots/.gitkeep0
-rw-r--r--screenshots/LoginLdap_Admin_admin_page.pngbin0 -> 328416 bytes
-rw-r--r--templates/index.twig300
-rw-r--r--tests/Integration/ApiTest.php216
-rw-r--r--tests/Integration/AuthenticationTest.php219
-rw-r--r--tests/Integration/BackwardsCompatibilityTest.php47
-rw-r--r--tests/Integration/Commands/SynchronizeUsersTest.php141
-rw-r--r--tests/Integration/LdapIntegrationTest.php155
-rw-r--r--tests/Integration/LdapUserSynchronizationTest.php313
-rw-r--r--tests/Integration/MultipleServersTest.php79
-rw-r--r--tests/Integration/SynchronizedAuthTest.php210
-rw-r--r--tests/Integration/WebServerAuthTest.php145
-rw-r--r--tests/Mocks/LdapFunctions.php76
-rw-r--r--tests/System/SystemAuthTest.php189
-rw-r--r--tests/UI/Admin_spec.js53
-rw-r--r--tests/UI/expected-ui-screenshots/LoginLdap_Admin_admin_page.pngbin0 -> 514853 bytes
-rw-r--r--tests/Unit/LdapClientTest.php337
-rw-r--r--tests/Unit/LdapUsersTest.php522
-rw-r--r--tests/Unit/UserAccessAttributeParserTest.php308
-rw-r--r--tests/Unit/UserAccessMapperTest.php180
-rw-r--r--tests/Unit/UserMapperTest.php292
-rw-r--r--tests/Unit/UserSynchronizerTest.php228
-rw-r--r--tests/travis/after_script.after.yml1
-rw-r--r--tests/travis/install.after.yml1
-rwxr-xr-xtests/travis/setup_ldap.sh305
101 files changed, 11801 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..32a26f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/tests/UI/processed-ui-screenshots/ \ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9c0391d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,133 @@
+# do not edit this file manually, instead run the generate:travis-yml console command
+
+language: php
+
+php:
+ - 5.6
+ - 5.5
+# - hhvm
+
+services:
+ - redis-server
+
+addons:
+ apt:
+ sources:
+ - deadsnakes
+
+ packages:
+ - python2.6
+ - python2.6-dev
+ - nginx
+ - realpath
+ - lftp
+
+git:
+ lfs_skip_smudge: true
+
+# Separate different test suites
+env:
+ global:
+ - PLUGIN_NAME=LoginLdap
+ - PIWIK_ROOT_DIR=$TRAVIS_BUILD_DIR/piwik
+ # this variable controls the version of Piwik your tests will run against.
+ # by default it will run against the maximum support version read from plugin.json
+ # (PIWIK_TEST_TARGET=maximum_supported_piwik).
+ # You can also specify a specific Piwik version
+ # (PIWIK_TEST_TARGET=2.16.0-b1).
+ - PIWIK_TEST_TARGET=maximum_supported_piwik
+ matrix:
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_TEST_TARGET
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ - TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_TEST_TARGET
+
+matrix:
+ exclude:
+ # execute latest stable tests only w/ PHP 5.6
+ - php: 5.5
+ env: TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ # execute UI tests only w/ PHP 5.6
+ - php: 5.5
+ env: TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_TEST_TARGET
+
+dist: trusty
+
+sudo: required
+
+script: $PIWIK_ROOT_DIR/tests/travis/travis.sh
+
+before_install:
+
+install:
+ # move all contents of current repo (which contains the plugin) to a new directory
+ - mkdir $PLUGIN_NAME
+ - cp -R !($PLUGIN_NAME) $PLUGIN_NAME
+ - cp -R .git/ $PLUGIN_NAME/
+ - cp .travis.yml $PLUGIN_NAME
+ # checkout piwik in the current directory
+ - git clone -q https://github.com/piwik/piwik.git piwik
+ - cd piwik
+ - git fetch -q --all
+ - git submodule update
+
+ # make sure travis-scripts repo is latest for initial travis setup
+ - '[ -d ./tests/travis/.git ] || sh -c "rm -rf ./tests/travis && git clone https://github.com/piwik/travis-scripts.git ./tests/travis"'
+ - cd ./tests/travis ; git checkout master ; cd ../..
+
+ - export GENERATE_TRAVIS_YML_COMMAND="php ./tests/travis/generator/main.php generate:travis-yml --plugin=\"LoginLdap\" --dist-trusty --verbose"
+ - '[[ "$TRAVIS_JOB_NUMBER" != *.1 || "$TRAVIS_PULL_REQUEST" != "false" ]] || ./tests/travis/autoupdate_travis_yml.sh'
+
+ - ./tests/travis/checkout_test_against_branch.sh
+
+ - '[ "$PLUGIN_NAME" == "" ] || [ ! -f ./tests/travis/check_plugin_compatible_with_piwik.php ] || php ./tests/travis/check_plugin_compatible_with_piwik.php "$PLUGIN_NAME"'
+
+ - ./tests/travis/configure_git.sh
+
+ # travis now complains about this failing 9 times out of 10, so removing it
+ #- travis_retry composer self-update
+ - '[ "$SKIP_COMPOSER_INSTALL" == "1" ] || travis_retry composer install'
+
+
+ # move plugin contents to folder in the plugins subdirectory
+ - rm -rf plugins/$PLUGIN_NAME
+ - mv ../$PLUGIN_NAME plugins
+
+ # clone dependent repos
+ - ./tests/travis/checkout_dependent_plugins.sh
+
+ - ./plugins/LoginLdap/tests/travis/setup_ldap.sh
+before_script:
+ - if [[ "$TRAVIS_PHP_VERSION" != 7* ]]; then phpenv config-rm xdebug.ini; fi
+
+ # add always_populate_raw_post_data=-1 to php.ini
+ - echo "always_populate_raw_post_data=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # disable opcache to avoid random failures on travis
+ - echo "opcache.enable=0" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # make tmpfs and run MySQL on it for reasonnable performance
+ - ./tests/travis/setup_mysql_tmpfs.sh
+
+ - ./tests/travis/prepare.sh
+ - ./tests/travis/setup_webserver.sh
+ - ./tests/travis/install_phantomjs.sh; export PATH=$PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH;
+
+ - cd tests/PHPUnit
+
+after_script:
+ # change directory back to root travis dir
+ - cd $PIWIK_ROOT_DIR
+
+ # output contents of files w/ debugging info to screen
+ - cat $PIWIK_ROOT_DIR/tests/travis/error.log
+ - cat $PIWIK_ROOT_DIR/tmp/php-fpm.log
+ - cat $PIWIK_ROOT_DIR/tmp/logs/piwik.log
+ - cat $PIWIK_ROOT_DIR/config/config.ini.php
+
+ # upload test artifacts (for debugging travis failures)
+ - ./tests/travis/upload_artifacts.sh
+
+ - sudo grep slapd /var/log/syslog
+
+after_success:
+ - cd $PIWIK_ROOT_DIR
diff --git a/API.php b/API.php
new file mode 100644
index 0000000..682b082
--- /dev/null
+++ b/API.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Piwik\Common;
+use Piwik\Piwik;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Exception;
+
+/**
+ */
+class API extends \Piwik\Plugin\API
+{
+ /**
+ * The LdapUsers instance to use when executing LDAP logic regarding LDAP users.
+ *
+ * @var LdapUsers
+ */
+ private $ldapUsers;
+
+ /**
+ * The UserSynchronizer instance to use when synchronizing users.
+ *
+ * @var UserSynchronizer
+ */
+ private $userSynchronizer;
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ $this->ldapUsers = LdapUsers::makeConfigured();
+ $this->userSynchronizer = UserSynchronizer::makeConfigured();
+ }
+
+ /**
+ * Saves LoginLdap config.
+ *
+ * @param string $data JSON encoded config array.
+ * @return array
+ * @throws Exception if user does not have super access, if this is not a POST method or
+ * if JSON is not supplied.
+ */
+ public function saveLdapConfig($data)
+ {
+ $this->checkHttpMethodIsPost();
+ Piwik::checkUserHasSuperUserAccess();
+
+ $data = json_decode(Common::unsanitizeInputValue($data), true);
+
+ Config::savePluginOptions($data);
+
+ return array('result' => 'success', 'message' => Piwik::translate("General_YourChangesHaveBeenSaved"));
+ }
+
+ /**
+ * Saves LDAP server config.
+ *
+ * @param string $data JSON encoded array w/ server info.
+ * @return array
+ * @throws Exception
+ */
+ public function saveServersInfo($data)
+ {
+ $this->checkHttpMethodIsPost();
+ Piwik::checkUserHasSuperUserAccess();
+
+ $servers = json_decode(Common::unsanitizeInputValue($data), true);
+
+ Config::saveLdapServerConfigs($servers);
+
+ return array('result' => 'success', 'message' => Piwik::translate("General_YourChangesHaveBeenSaved"));
+ }
+
+ /**
+ * Returns count of users in LDAP that are member of a specific group of names. Uses a search
+ * filter with memberof=?.
+ *
+ * @param string $memberOf The group to check.
+ * @return int
+ * @throws Exception if the current user is not a Super User or something goes wrong with LDAP.
+ */
+ public function getCountOfUsersMemberOf($memberOf)
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $memberOf = Common::unsanitizeInputValue($memberOf);
+
+ $memberOfField = Config::getRequiredMemberOfField();
+
+ return $this->ldapUsers->getCountOfUsersMatchingFilter("(".$memberOfField."=?)", array($memberOf));
+ }
+
+ /**
+ * Returns count of users in LDAP that match an LDAP filter. If the filter is incorrect,
+ * `null` is returned.
+ *
+ * @param string $filter The filter to match.
+ * @return int|null
+ * @throws Exception if the current user is not a Super User or something goes wrong with LDAP.
+ */
+ public function getCountOfUsersMatchingFilter($filter)
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $filter = Common::unsanitizeInputValue($filter);
+
+ try {
+ return $this->ldapUsers->getCountOfUsersMatchingFilter($filter);
+ } catch (Exception $ex) {
+ if (stripos($ex->getMessage(), "Bad search filter") !== false) {
+ throw new Exception(Piwik::translate("LoginLdap_InvalidFilter"));
+ } else {
+ throw $ex;
+ }
+ }
+ }
+
+ /**
+ * Synchronizes a single user in LDAP. This method can be used by superusers to synchronize
+ * a user before (s)he logs in.
+ *
+ * @param string $login The login of the user.
+ * @throws Exception if the user cannot be found or a problem occurs during synchronization.
+ */
+ public function synchronizeUser($login)
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $ldapUser = $this->ldapUsers->getUser($login);
+ if (empty($ldapUser)) {
+ throw new Exception(Piwik::translate('LoginLdap_UserNotFound', $login));
+ }
+
+ $this->userSynchronizer->synchronizeLdapUser($login, $ldapUser);
+ $this->userSynchronizer->synchronizePiwikAccessFromLdap($login, $ldapUser);
+ }
+
+ private function checkHttpMethodIsPost()
+ {
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ throw new Exception("Invalid HTTP method.");
+ }
+ }
+} \ No newline at end of file
diff --git a/Auth/Base.php b/Auth/Base.php
new file mode 100644
index 0000000..c0f070f
--- /dev/null
+++ b/Auth/Base.php
@@ -0,0 +1,402 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Auth;
+
+use Exception;
+use Piwik\Auth;
+use Piwik\AuthResult;
+use Piwik\Container\StaticContainer;
+use Piwik\Piwik;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\UsersManager\Model as UserModel;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Base class for LoginLdap authentication implementations.
+ */
+abstract class Base implements Auth
+{
+ /**
+ * The username to authenticate with.
+ *
+ * @var null|string
+ */
+ protected $login = null;
+
+ /**
+ * The token auth to authenticate with.
+ *
+ * @var null|string
+ */
+ protected $token_auth = null;
+
+ /**
+ * The password to authenticate with (unhashed).
+ *
+ * @var null|string
+ */
+ protected $password = null;
+
+ /**
+ * The password hash to authenticate with.
+ *
+ * @var string
+ */
+ private $passwordHash = null;
+
+ /**
+ * LdapUsers DAO instance.
+ *
+ * @var LdapUsers
+ */
+ protected $ldapUsers;
+
+ /**
+ * Piwik Users model. Used to query for data in the Piwik users table.
+ *
+ * @var UserModel
+ */
+ protected $usersModel;
+
+ /**
+ * UserSynchronizer instance used to convert LDAP users to Piwik users and then
+ * persist them in Piwik's MySQL database. Doing so allows Piwik to authorize and
+ * authenticate LDAP users without having to communicate with the LDAP server
+ * on each request.
+ *
+ * @var UserSynchronizer
+ */
+ protected $userSynchronizer;
+
+ /**
+ * UsersManager API instance.
+ *
+ * @var UsersManagerAPI
+ */
+ protected $usersManagerAPI;
+
+ /**
+ * Cache of user info for the current user being authenticated. This is the result of
+ * UserModel::getUser().
+ *
+ * @var string[]
+ */
+ protected $userForLogin = null;
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Authentication module's name, e.g., "LoginLdap"
+ *+
+ * @return string
+ */
+ public function getName()
+ {
+ return 'LoginLdap';
+ }
+
+ /**
+ * Sets the password to authenticate with.
+ *
+ * @param string $password password
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ }
+
+ /**
+ * Sets the authentication token to authenticate with.
+ *
+ * @param string $token_auth authentication token
+ */
+ public function setTokenAuth($token_auth)
+ {
+ $this->token_auth = $token_auth;
+ }
+
+ /**
+ * Sets the hash of the password to authenticate with. The hash will be an MD5 hash.
+ *
+ * @param string $passwordHash The hashed password.
+ * @throws Exception if authentication by hashed password is not supported.
+ */
+ public function setPasswordHash($passwordHash)
+ {
+ $this->passwordHash = $passwordHash;
+ }
+
+ /**
+ * Returns the login of the user being authenticated.
+ *
+ * @return string
+ */
+ public function getLogin()
+ {
+ return $this->login;
+ }
+
+ /**
+ * Returns the user's token auth.
+ *
+ * @return string
+ */
+ public function getTokenAuth()
+ {
+ if (!empty($this->token_auth)) {
+ return $this->token_auth;
+ }
+
+ if (!empty($this->login) && $tokenAuthSecret = $this->getTokenAuthSecret()) {
+ return $this->usersManagerAPI->getTokenAuth($this->login, $tokenAuthSecret);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the secret used to calculate a user's token auth.
+ *
+ * @return string|null
+ */
+ public function getTokenAuthSecret()
+ {
+ if (!empty($this->passwordHash)) {
+ return $this->passwordHash;
+ }
+
+ if (!empty($this->password)) {
+ return md5($this->password);
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the login name to authenticate with.
+ *
+ * @param string $login The username.
+ */
+ public function setLogin($login)
+ {
+ $this->login = $login;
+ }
+
+ /**
+ * Gets the {@link $ldapUsers} property.
+ *
+ * @return LdapUsers
+ */
+ public function getLdapUsers()
+ {
+ return $this->ldapUsers;
+ }
+
+ /**
+ * Sets the {@link $ldapUsers} property.
+ *
+ * @param LdapUsers $ldapUsers
+ */
+ public function setLdapUsers($ldapUsers)
+ {
+ $this->ldapUsers = $ldapUsers;
+ }
+
+ /**
+ * Gets the {@link $usersModel} property.
+ *
+ * @return \Piwik\Plugins\UsersManager\Model
+ */
+ public function getUsersModel()
+ {
+ return $this->usersModel;
+ }
+
+ /**
+ * Sets the {@link $usersModel} property.
+ *
+ * @param \Piwik\Plugins\UsersManager\Model $usersModel
+ */
+ public function setUsersModel($usersModel)
+ {
+ $this->usersModel = $usersModel;
+ }
+
+ /**
+ * Gets the {@link $userSynchronizer} property.
+ *
+ * @return UserSynchronizer
+ */
+ public function getUserSynchronizer()
+ {
+ return $this->userSynchronizer;
+ }
+
+ /**
+ * Sets the {@link $userSynchronizer} property.
+ *
+ * @param UserSynchronizer $userSynchronizer
+ */
+ public function setUserSynchronizer($userSynchronizer)
+ {
+ $this->userSynchronizer = $userSynchronizer;
+ }
+
+ /**
+ * Gets the {@link $usersManagerAPI} property.
+ *
+ * @return UsersManagerAPI
+ */
+ public function getUsersManagerAPI()
+ {
+ return $this->usersManagerAPI;
+ }
+
+ /**
+ * Sets the {@link $usersManagerAPI} property.
+ *
+ * @param UsersManagerAPI $usersManagerAPI
+ */
+ public function setUsersManagerAPI($usersManagerAPI)
+ {
+ $this->usersManagerAPI = $usersManagerAPI;
+ }
+
+ protected function getUserForLogin()
+ {
+ if (empty($this->userForLogin)) {
+ if (!empty($this->login)) {
+ $this->userForLogin = $this->usersModel->getUser($this->login);
+ } else if (!empty($this->token_auth)) {
+ $this->userForLogin = $this->usersModel->getUserByTokenAuth($this->token_auth);
+ } else {
+ throw new Exception("Cannot get user details, neither login nor token auth are set.");
+ }
+ }
+ return $this->userForLogin;
+ }
+
+ protected function tryFallbackAuth($onlySuperUsers = true, Auth $auth = null)
+ {
+ if (empty($auth)) {
+ $auth = new \Piwik\Plugins\Login\Auth();
+ } else {
+ $this->logger->debug("Auth\\Base::{func}: trying fallback auth with auth implementation '{impl}'", array(
+ 'func' => __FUNCTION__,
+ 'impl' => get_class($auth)
+ ));
+ }
+
+ $auth->setLogin($this->login);
+ if (!empty($this->password)) {
+ $this->logger->debug("Auth\\Base::{func}: trying normal auth with user password", array('func' => __FUNCTION__));
+
+ $auth->setPassword($this->password);
+ } else if (!empty($this->passwordHash)) {
+ $this->logger->debug("Auth\\Base::{func}: trying normal auth with hashed password", array('func' => __FUNCTION__));
+
+ $auth->setPasswordHash($this->passwordHash);
+ } else {
+ $this->logger->debug("Auth\\Base::{func}: trying normal auth with token auth", array('func' => __FUNCTION__));
+
+ $auth->setTokenAuth($this->getTokenAuth());
+ }
+ $result = $auth->authenticate();
+
+ $this->logger->debug("Auth\\Base::{func}: normal auth returned result code {code} for user '{login}'", array(
+ 'func' => __FUNCTION__,
+ 'code' => $result->getCode(),
+ 'login' => $this->login
+ ));
+
+ if (!$onlySuperUsers
+ || $result->getCode() == AuthResult::SUCCESS_SUPERUSER_AUTH_CODE
+ ) {
+ return $result;
+ } else {
+ return $this->makeAuthFailure();
+ }
+ }
+
+ protected function synchronizeLdapUser($ldapUser)
+ {
+ $this->userForLogin = $this->userSynchronizer->synchronizeLdapUser($this->login, $ldapUser);
+ $this->userSynchronizer->synchronizePiwikAccessFromLdap($this->login, $ldapUser);
+ }
+
+ protected function makeSuccessLogin($userInfo)
+ {
+ $successCode = $userInfo['superuser_access'] ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS;
+
+ if ($userInfo['token_auth']) {
+ $tokenAuth = $userInfo['token_auth'];
+ } else {
+ $tokenAuth = $this->getTokenAuth();
+
+ if (empty($userInfo['login']) || empty($tokenAuth)) {
+ throw new Exception('User couldn\'t be found');
+ }
+ }
+
+ return new AuthResult($successCode, $userInfo['login'], $tokenAuth);
+ }
+
+ protected function makeAuthFailure()
+ {
+ return new AuthResult(AuthResult::FAILURE, $this->login, $this->getTokenAuth());
+ }
+
+ protected function authenticateByLdap()
+ {
+ $this->checkLdapFunctionsAvailable();
+
+ $ldapUser = $this->ldapUsers->authenticate($this->login, $this->password);
+ if (!empty($ldapUser)) {
+ $this->synchronizeLdapUser($ldapUser);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private function checkLdapFunctionsAvailable()
+ {
+ if (!function_exists('ldap_connect')) {
+ throw new Exception(Piwik::translate('LoginLdap_LdapFunctionsMissing'));
+ }
+ }
+
+ /**
+ * Returns the authentication implementation to use in LoginLdap based on certain
+ * INI configuration values.
+ *
+ * @return Base
+ */
+ public static function factory()
+ {
+ if (Config::shouldUseWebServerAuthentication()) {
+ return WebServerAuth::makeConfigured();
+ } else if (Config::getUseLdapForAuthentication()) {
+ return LdapAuth::makeConfigured();
+ } else {
+ return SynchronizedAuth::makeConfigured();
+ }
+ }
+} \ No newline at end of file
diff --git a/Auth/LdapAuth.php b/Auth/LdapAuth.php
new file mode 100644
index 0000000..6ca0905
--- /dev/null
+++ b/Auth/LdapAuth.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Auth;
+
+use Exception;
+use Piwik\AuthResult;
+use Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\UsersManager\Model as UserModel;
+
+/**
+ * LDAP based authentication implementation: allows authenticating to Piwik via
+ * an LDAP server.
+ *
+ * Supports authenticating by login and password, and supports authenticating by
+ * token auth (with login or without).
+ *
+ * ## Implementation Details
+ *
+ * **Authenticating By Password**
+ *
+ * When authenticating by password LdapAuth will communicate with the LDAP server.
+ * On a successful authentication, the details of the LDAP user will be synchronized
+ * in Piwik's DB (except the password). This is so Piwik can be personalized for
+ * the user without having to communicate w/ the LDAP server on every request.
+ *
+ * If the user does not exist in the MySQL DB on first authentication, it will be
+ * created. If it does exist, it will be updated. This way, changes made in the
+ * LDAP server will be reflected in the UI.
+ *
+ * **Authenticating By Token Auth**
+ *
+ * Authenticating by token auth is more complicated than by authenticating by password.
+ * There is no LDAP concept of a authentication token, and connecting to the LDAP
+ * server for every token auth authentication would be very wasteful.
+ *
+ * So instead, when a user is synchronized, a token auth is generated using part of
+ * the password hash stored in LDAP. We don't want to store the whole password hash
+ * so attackers cannot get the true hash if they gain access to the MySQL DB.
+ *
+ * Once the token auth is generated, authenticating with it is done in the same way
+ * as with {@link Piwik\Plugins\Login\Auth}. In fact, this class will create an
+ * instance of that one to authenticate.
+ *
+ * **Non-LDAP Users**
+ *
+ * After LoginLdap is enabled, normal Piwik users are not allowed to authenticate.
+ * Only normal super users so the plugin can be managed w/o LDAP users existing.
+ *
+ * **Default View Access**
+ *
+ * When a user is created in Piwik, (s)he must be provided with access to at least
+ * one website. The website(s) new users are given access to is determined by the
+ * `[LoginLdap] new_user_default_sites_view_access` INI config option.
+ */
+class LdapAuth extends Base
+{
+ /**
+ * Sets the hash of the password to authenticate with. The hash will be an MD5 hash.
+ *
+ * @param string $passwordHash The hashed password.
+ * @throws Exception if authentication by hashed password is not supported.
+ */
+ public function setPasswordHash($passwordHash)
+ {
+ if ($passwordHash !== null) {
+ throw new Exception("Authentication by password hash is not supported when authenticating by LDAP.");
+ }
+ }
+
+ /**
+ * Attempts to authenticate with the information set on this instance.
+ *
+ * @return AuthResult
+ */
+ public function authenticate()
+ {
+ try {
+ $result = $this->tryFallbackAuth($onlySuperUsers = false);
+ if ($result->wasAuthenticationSuccessful()) {
+ return $result;
+ }
+
+ if (empty($this->login)) { // occurs on API requests since FrontController will still reloadAccess
+ $this->logger->debug("authenticateByPassword: empty login encountered");
+
+ return $this->makeAuthFailure();
+ }
+
+ if ($this->login == 'anonymous') { // sanity check
+ $this->logger->debug("authenticateByPassword: login is 'anonymous'");
+
+ return $this->makeAuthFailure();
+ }
+
+ $authenticationSucceeded = $this->authenticateByLdap();
+
+ if ($authenticationSucceeded) {
+ return $this->makeSuccessLogin($this->getUserForLogin());
+ } else {
+ return $this->makeAuthFailure();
+ }
+ } catch (ConnectionException $ex) {
+ throw $ex;
+ } catch (Exception $ex) {
+ $this->logger->debug("LdapAuth::{func} failed: {message}", array(
+ 'func' => __FUNCTION__,
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+
+ return $this->makeAuthFailure();
+ }
+
+ /**
+ * Returns a WebServerAuth instance configured with INI config.
+ *
+ * @return LdapAuth
+ */
+ public static function makeConfigured()
+ {
+ $result = new LdapAuth();
+ $result->setLdapUsers(LdapUsers::makeConfigured());
+ $result->setUsersManagerAPI(UsersManagerAPI::getInstance());
+ $result->setUsersModel(new UserModel());
+ $result->setUserSynchronizer(UserSynchronizer::makeConfigured());
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/Auth/SynchronizedAuth.php b/Auth/SynchronizedAuth.php
new file mode 100644
index 0000000..7a3eea9
--- /dev/null
+++ b/Auth/SynchronizedAuth.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Auth;
+
+use Exception;
+use Piwik\Auth\Password;
+use Piwik\AuthResult;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\UsersManager\Model as UserModel;
+use Piwik\Plugins\UsersManager\UsersManager;
+
+/**
+ * Auth implementation that only uses LDAP to synchronize user details.
+ *
+ * Supports authenticating by password, authenticating by token auth and authenticating
+ * by password hash.
+ *
+ * ## Implementation Details
+ *
+ * SynchronizedAuth uses the normal Piwik authentication class (Piwik\Plugins\Login\Auth).
+ * If login via this class fails, then SynchronizedAuth tries to login via LDAP, and on
+ * success, synchronizes user details, including the password in LDAP.
+ *
+ * This means that if this auth implementation is used, the password will be stored in
+ * Piwik's DB in addition to LDAP.
+ *
+ * Synchronizing after login can be disabled via the `[LoginLdap] synchronize_users_after_login` option.
+ *
+ * Note: A user's password will always be updated after a successful LDAP login, since
+ * if we can't authenticate normally for the user, the password has changed in LDAP.
+ *
+ * Users that do not exist in LDAP, but exist in Piwik's DB will be able to authenticate.
+ */
+class SynchronizedAuth extends Base
+{
+ /**
+ * Whether a user's LDAP information should be synchronized with Piwik's DB after each
+ * successful login or not.
+ *
+ * @var bool
+ */
+ private $synchronizeUsersAfterSuccessfulLogin = true;
+
+ /**
+ * Attempts to authenticate with the information set on this instance.
+ *
+ * @return AuthResult
+ */
+ public function authenticate()
+ {
+ try {
+ $result = $this->tryFallbackAuth($onlySuperUsers = false);
+ if ($result->wasAuthenticationSuccessful()) {
+ return $result;
+ }
+
+ if (!$this->synchronizeUsersAfterSuccessfulLogin) {
+ $this->logger->debug("SynchronizedAuth::{func}: synchronizing users after login disabled, not attempting LDAP authenticate for '{login}'.",
+ array('func' => __FUNCTION__, 'login' => $this->login));
+
+ return $this->makeAuthFailure();
+ }
+
+ if (empty($this->password)) {
+ $this->logger->debug("SynchronizedAuth::{func}: cannot attempt fallback LDAP login for '{login}', password not set.",
+ array('func' => __FUNCTION__, 'login' => $this->login));
+
+ return $this->makeAuthFailure();
+ }
+
+ $successful = $this->authenticateByLdap();
+ if ($successful) {
+ $this->updateUserPassword();
+
+ return $this->makeSuccessLogin($this->getUserForLogin());
+ } else {
+ return $this->makeAuthFailure();
+ }
+ } catch (ConnectionException $ex) {
+ throw $ex;
+ } catch (Exception $ex) {
+ $this->logger->debug("SynchronizedAuth::{func} failed: {message}", array(
+ 'func' => __FUNCTION__,
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+
+ return $this->makeAuthFailure();
+ }
+
+ /**
+ * Gets {@link $synchronizeUsersAfterSuccessfulLogin} property.
+ *
+ * @return boolean
+ */
+ public function isSynchronizeUsersAfterSuccessfulLogin()
+ {
+ return $this->synchronizeUsersAfterSuccessfulLogin;
+ }
+
+ /**
+ * Sets {@link $synchronizeUsersAfterSuccessfulLogin} property.
+ *
+ * @param boolean $synchronizeUsersAfterSuccessfulLogin
+ */
+ public function setSynchronizeUsersAfterSuccessfulLogin($synchronizeUsersAfterSuccessfulLogin)
+ {
+ $this->synchronizeUsersAfterSuccessfulLogin = $synchronizeUsersAfterSuccessfulLogin;
+ }
+
+ private function updateUserPassword()
+ {
+ $user = $this->getUserForLogin();
+
+ $passwordHelper = new Password();
+ $passwordHash = $passwordHelper->hash(UsersManager::getPasswordHash($this->password));
+ $this->usersModel->updateUser($this->login, $passwordHash, $user['email'], $user['alias'], $user['token_auth']);
+
+ $this->userForLogin['password'] = $passwordHash;
+ }
+
+ /**
+ * Returns a WebServerAuth instance configured with INI config.
+
+ * @return SynchronizedAuth
+ */
+ public static function makeConfigured()
+ {
+ $result = new SynchronizedAuth();
+ $result->setLdapUsers(LdapUsers::makeConfigured());
+ $result->setUsersManagerAPI(UsersManagerAPI::getInstance());
+ $result->setUsersModel(new UserModel());
+ $result->setUserSynchronizer(UserSynchronizer::makeConfigured());
+
+ $synchronizeUsersAfterSuccessfulLogin = Config::getShouldSynchronizeUsersAfterLogin();
+ $result->setSynchronizeUsersAfterSuccessfulLogin($synchronizeUsersAfterSuccessfulLogin);
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/Auth/WebServerAuth.php b/Auth/WebServerAuth.php
new file mode 100644
index 0000000..90d245b
--- /dev/null
+++ b/Auth/WebServerAuth.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Auth;
+
+use Exception;
+use Piwik\Auth;
+use Piwik\AuthResult;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\UsersManager\Model as UserModel;
+
+/**
+ * Auth implementation that assumes the web server that hosts Piwik has authenticated
+ * users.
+ *
+ * Supports every type of authentication since authentication is delegated to the web server.
+ *
+ * ## Implementation Details
+ *
+ * Checks for the $_SERVER['REMOTE_USER'] variable, if present assumes the user was authenticated
+ * by the web server.
+ *
+ * This auth implementation will still connect to LDAP in order to synchronize user details.
+ *
+ * If the `[LoginLdap] synchronize_users_after_login` option is set to 0, synchronization
+ * will not occur after login.
+ */
+class WebServerAuth extends Base
+{
+ /**
+ * Whether a user's LDAP information should be synchronized with Piwik's DB after each
+ * successful login or not.
+ *
+ * @var bool
+ */
+ private $synchronizeUsersAfterSuccessfulLogin = true;
+
+ /**
+ * Fallback LDAP Auth implementation to use if REMOTE_USER is not found.
+ *
+ * @var Auth
+ */
+ private $fallbackAuth;
+
+ /**
+ * Attempts to authenticate with the information set on this instance.
+ *
+ * @return AuthResult
+ */
+ public function authenticate()
+ {
+ try {
+ $webServerAuthUser = $this->getAlreadyAuthenticatedLogin();
+
+ if (empty($webServerAuthUser)) {
+ $this->logger->debug("using web server authentication, but REMOTE_USER server variable not found.");
+
+ return $this->tryFallbackAuth($onlySuperUsers = false, $this->fallbackAuth);
+ } else {
+ $this->login = preg_replace('/@.*/', '', $webServerAuthUser);
+ $this->password = '';
+
+ $this->logger->info("User '{login}' authenticated by webserver.", array('login' => $this->login));
+
+ if ($this->synchronizeUsersAfterSuccessfulLogin) {
+ $this->synchronizeLoggedInUser();
+ } else {
+ $this->logger->debug("WebServerAuth::{func}: not synchronizing user '{login}'.", array(
+ 'func' => __FUNCTION__,
+ 'login' => $this->login
+ ));
+ }
+
+ return $this->makeSuccessLogin($this->getUserForLogin());
+ }
+ } catch (ConnectionException $ex) {
+ throw $ex;
+ } catch (Exception $ex) {
+ $this->logger->debug("WebServerAuth::{func} failed: {message}", array(
+ 'func' => __FUNCTION__,
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+
+ return $this->makeAuthFailure();
+ }
+
+ /**
+ * Gets the {@link $synchronizeUsersAfterSuccessfulLogin} property.
+ *
+ * @return boolean
+ */
+ public function isSynchronizeUsersAfterSuccessfulLogin()
+ {
+ return $this->synchronizeUsersAfterSuccessfulLogin;
+ }
+
+ /**
+ * Sets the {@link $synchronizeUsersAfterSuccessfulLogin} property.
+ *
+ * @param boolean $synchronizeUsersAfterSuccessfulLogin
+ */
+ public function setSynchronizeUsersAfterSuccessfulLogin($synchronizeUsersAfterSuccessfulLogin)
+ {
+ $this->synchronizeUsersAfterSuccessfulLogin = $synchronizeUsersAfterSuccessfulLogin;
+ }
+
+ /**
+ * Gets the {@link $fallbackAuth} property.
+ *
+ * @return Auth
+ */
+ public function getFallbackAuth()
+ {
+ return $this->fallbackAuth;
+ }
+
+ /**
+ * Sets the {@link $fallbackAuth} property.
+ *
+ * @param Auth $fallbackAuth
+ */
+ public function setFallbackAuth($fallbackAuth)
+ {
+ $this->fallbackAuth = $fallbackAuth;
+ }
+
+ private function getAlreadyAuthenticatedLogin()
+ {
+ return @$_SERVER['REMOTE_USER'];
+ }
+
+ private function synchronizeLoggedInUser()
+ {
+ $ldapUser = $this->ldapUsers->getUser($this->login);
+
+ if (empty($ldapUser)) {
+ $this->logger->warning("Cannot find web server authenticated user {login} in LDAP!", array('login' => $this->login));
+ return;
+ }
+
+ $this->synchronizeLdapUser($ldapUser);
+ }
+
+ /**
+ * Returns a WebServerAuth instance configured with INI config.
+ *
+ * @return WebServerAuth
+ */
+ public static function makeConfigured()
+ {
+ $result = new WebServerAuth();
+ $result->setLdapUsers(LdapUsers::makeConfigured());
+ $result->setUsersManagerAPI(UsersManagerAPI::getInstance());
+ $result->setUsersModel(new UserModel());
+ $result->setUserSynchronizer(UserSynchronizer::makeConfigured());
+
+ $synchronizeUsersAfterSuccessfulLogin = Config::getShouldSynchronizeUsersAfterLogin();
+ $result->setSynchronizeUsersAfterSuccessfulLogin($synchronizeUsersAfterSuccessfulLogin);
+
+ if (Config::getUseLdapForAuthentication()) {
+ $fallbackAuth = LdapAuth::makeConfigured();
+ } else {
+ $fallbackAuth = SynchronizedAuth::makeConfigured();
+ }
+
+ $result->setFallbackAuth($fallbackAuth);
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..37a4e34
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,172 @@
+# LoginLdap Changelog
+
+#### LoginLdap 4.0.4
+
+* Fixing bug that made it impossible to set append_user_email_suffix_to_username to 0 for appending username suffix to username for email and not during auth.
+
+#### LoginLdap 4.0.0
+
+* Compatibility with Piwik 3
+* Configuration value 'enable_random_token_auth_generation' has been removed as its obsolete with Piwik 3 having random auth tokens.
+* Command `loginldap:generate-token-auth` has been removed as auth tokens are independent from password now and new auth token can now be generated directly in user admin
+* Updated UI: Now completely works using AngularJS and material design
+
+#### LoginLdap 3.3.1
+
+* Plugin settings: clarify an inline help for `Use Web Server Auth (e.g. Kerberos SSO)`
+
+#### LoginLdap 3.3.0
+
+* Compatibility with Piwik 2.16.0
+
+#### LoginLdap 3.2.2
+
+* LDAP user can't change their passwords in Piwik's UI (passwords should be managed directly on LDAP host)
+
+#### LoginLdap 3.2.1
+
+* Configureed LDAP passwords are no longer stored in the HTML in the LDAP settings page. This is a minor security update.
+
+#### LoginLdap 3.2.0
+
+* Compatibility w/ Piwik 2.15.0
+
+#### LoginLdap 3.1.5
+
+* Fixing regression caused by Piwik 2.14 change: authenticating in tracker w/ token_auth no longer worked if LoginLdap was used.
+* Workaround issue where 'LDAP Functions are Missing' notification was never removed from the screen by making it transient & closeable.
+
+#### LoginLdap 3.1.2
+
+* Change placeholder value of server hostname config option and add a note so users can avoid the problem where ports are ignored when ldap:// URLs are used in the hostname option.
+* Make sure users upgrading from pre-3.0 versions set the correct LDAP settings.
+* Add documentation regarding using LoginLdap with Piwik's official mobile app.
+
+#### LoginLdap 3.1.1
+
+* Make plugin compatible with latest Piwik version.
+
+#### LoginLdap 3.1.0
+
+* add --skip-existing option to loginldap:synchronize-users command
+* warning displayed if Login + LoginLdap plugins are enabled at the same time
+* re-added the load ldap user form in the settings page
+* normal users can be managed when LdapAuth implementation is used (when Always Use LDAP for Authentication is checked)
+* fixed bug in web server auth strategy where LDAP auth was not used if REMOTE_USER var not found. made connecting via mobile app impossible.
+* fix bug in synchronizing users w/ user_email_suffix configured (first login worked, subsequent logins failed since username used in UserSynchronizer was incorrect)
+
+#### LoginLdap 3.0.0:
+
+* Automatic creation of Piwik users using LDAP (old 'auto create users' feature) is now standard.
+* Default access permissions can be specified for newly synchronized users.
+* Only super users are allowed to login w/o authenticating to LDAP now. Normal users stored in Piwik will not be allowed to authenticate if using LoginLdap.
+* It is possible now to test memberOf and filter settings from within the LDAP settings page.
+* Piwik access permissions can be specified from within LDAP using custom attributes.
+* It is allowed to specify multiple LDAP fallback servers. If one fails, the others are used.
+* Tests that make sure the PHP LDAP extension exists were fixed and also implemented in loginpage.
+* Special LDAP log was removed. Logging is done through Piwik\\Log now.
+* New setting for LDAP network timeout.
+* Menu entry is LDAP > Settings now instead of Manage > LDAP Users.
+* The synchronize single user feature in the settings page was removed.
+* Supports three types of authentication strategies.
+* Only compatible with Piwik 2.8 and above.
+
+#### LoginLdap 2.2.7:
+* Auto create users from LDAP #23
+
+#### LoginLdap 2.2.6:
+* Fixes empty characters
+
+#### LoginLdap 2.2.5:
+* Fixes issue #22 'unable to login'
+
+#### LoginLdap 2.2.4:
+* Added debug mode and more detail logging
+
+#### LoginLdap 2.2.3:
+* Fixes #21 Ensure all variables are correctly set
+* Storing log file in tmp/logs/ and fix PHP log read warning
+
+#### LoginLdap 2.2.2:
+* Adding missing namespace
+
+#### LoginLdap 2.2.1:
+* Controller now extends Login controller. Reusing assets and templates.
+
+#### LoginLdap 2.1.0:
+* Code updated to support Piwik 2.1 and newer
+
+#### LoginLdap 2.0.9:
+* Fixes Piwik #4001 Deprecate force_ssl_login setting
+
+#### LoginLdap 2.0.8:
+* Fixed issue #7 'Deinstallation not possible'
+
+#### LoginLdap 2.0.7:
+* Fixed issue #4 'useKerberos config problem'
+
+#### LoginLdap 2.0.6:
+* Tmuellerleile fixed default controller action
+
+#### LoginLdap 2.0.5:
+* Fixed issue with log file creation and reading
+
+#### LoginLdap 2.0.4:
+* Added 'View LDAP log from web as admin'
+* Added better error detection and check if LDAP is enabled in PHP
+
+#### LoginLdap 2.0.3:
+* Issue #26 Fixed 'malformed UTF8 in de.json'
+* Issue #28 Fixed 'plugin install should add parameters to config.ini.php'
+
+#### LoginLdap 2.0.2:
+* Added 'de' and 'et' translations
+* Minor code enhancements
+
+#### LoginLdap 2.0.1:
+* First public release in Piwik Marketplace
+
+#### LoginLdap 2.0.0:
+* First release for Piwik 2.0, may contain bugs!
+* Added LDAP server port configuration option
+
+#### LoginLdap 1.3.5:
+* Issue #20 Fixed 'kerberos is not working'
+* Issue #19 Fixed 'wrong version info'
+
+#### LoginLdap 1.3.4:
+* Issue #18 Fixed 'iconv() expects parameter 3 to be string array given'
+
+#### LoginLdap 1.3.3:
+* Issue #17 Fixed 'Undefined index: phpVersion'
+
+#### LoginLdap 1.3.2:
+* Issue #15 Fixed 'Setting a custom mail field has no effect'
+* Issue #16 Fixed 'Login fails because of non-UTF8 values passed to json_encode()'
+
+#### LoginLdap 1.3.1:
+* Issue #7 Added check on the activate handler to ensure the php-ldap extension is installed.
+* Issue #8 Only superuser can view (and modify) LDAP configuration
+* Issue #9 Fixed 'Undefined index: activeDirectory'
+* Issue #11 E-Mail Address Being Required
+* Issue #12 Fixed 'Undefined index: topMenu'
+* Issue #13 LDAP Users were not able login using the mobile app and using API in general as their credentials were not stored in the database.
+* Applied fix for Piwik Dev Zone Ticket #734: 'Correction added so Page Overlay feature works'.
+* Added functionality to ensure that the Login and LoginLDAP plugins are never enabled simultaneously.
+* Removed support for IE6.
+* Changed log file location so to be include into the plugin directory and more easy to find.
+
+#### LoginLdap 1.3.0:
+* Issue #1 Only superuser can modify LDAP configuration
+* Issue #2 LDAP search filters
+* Issue #3 Enable Kerberos login for piwik
+* Issue #4 You cannot login as superuser if LDAP connection fails
+* Issue #5 Add more LDAP logging options
+* Issue #6 Error while trying to read a specific config file entry 'LoginLdap'
+
+#### LoginLdap 1.2.0:
+* ActiveDirectory Support
+* Piwik >= 1.6 Install Bug Fix
+
+#### LoginLdap 1.0.0:
+* Initial Version just for plain anonymous Ldap
diff --git a/Commands/SynchronizeUsers.php b/Commands/SynchronizeUsers.php
new file mode 100644
index 0000000..3effd95
--- /dev/null
+++ b/Commands/SynchronizeUsers.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Commands;
+
+use Exception;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\LoginLdap\API as LoginLdapAPI;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Command to synchronize multiple users in LDAP w/ Piwik's MySQL DB. Can be used
+ * to make sure user information is synchronized before the user logs in the first time.
+ */
+class SynchronizeUsers extends ConsoleCommand
+{
+ /**
+ * @var LoginLdapAPI
+ */
+ private $loginLdapAPI;
+
+ /**
+ * @var LdapUsers
+ */
+ private $ldapUsers;
+
+ /**
+ * @var UsersManagerAPI
+ */
+ private $usersManagerAPI;
+
+ public function __construct($name = null)
+ {
+ parent::__construct($name);
+
+ $this->loginLdapAPI = LoginLdapAPI::getInstance();
+ $this->ldapUsers = LdapUsers::makeConfigured();
+ $this->usersManagerAPI = UsersManagerAPI::getInstance();
+ }
+
+ protected function configure()
+ {
+ $this->setName('loginldap:synchronize-users');
+ $this->setDescription('Synchronizes one, multiple or all LDAP users that the current config has access to.');
+ $this->addOption('login', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+ 'List of users to synchronize. If not specified, all users in LDAP are synchronized.');
+ $this->addOption('skip-existing', null, InputOption::VALUE_NONE,
+ "Skip users that have been synchronized at least once. Using this option will be much faster, but will not "
+ . "update user info if it has changed in LDAP.");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $logins = $input->getOption('login');
+ $skipExisting = $input->getOption('skip-existing');
+
+ if (empty($logins)) {
+ $logins = $this->ldapUsers->getAllUserLogins();
+ }
+
+ $count = 0;
+ $failed = array();
+
+ foreach ($logins as $login) {
+ if ($skipExisting
+ && $this->userExistsInPiwik($login)
+ ) {
+ $output->write("Skipping '$login', already exists in Piwik...");
+ continue;
+ }
+
+ $output->write("Synchronizing '$login'... ");
+
+ try {
+ $this->loginLdapAPI->synchronizeUser($login);
+
+ ++$count;
+
+ $output->writeln("<info>success!</info>");
+ } catch (Exception $ex) {
+ $failed[] = array('login' => $login, 'reason' => $ex->getMessage());
+
+ $output->writeln("<error>failed!</error>");
+ }
+ }
+
+ $this->writeSuccessMessage($output, array("Synchronized $count users!"));
+
+ if (!empty($failed)) {
+ $output->writeln("<info>Could not synchronize the following users in LDAP:</info>");
+ foreach ($failed as $missingLogin) {
+ $output->writeln($missingLogin['login'] . "\t\t<comment>{$missingLogin['reason']}</comment>");
+ }
+ }
+
+ return count($failed);
+ }
+
+ private function userExistsInPiwik($login)
+ {
+ return $this->usersManagerAPI->userExists($login);
+ }
+} \ No newline at end of file
diff --git a/Config.php b/Config.php
new file mode 100644
index 0000000..470154b
--- /dev/null
+++ b/Config.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Exception;
+use Piwik\Config as PiwikConfig;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\Ldap\Client;
+use Piwik\Plugins\LoginLdap\Ldap\ServerInfo;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Utility class with methods to manage LoginLdap INI configuration.
+ */
+class Config
+{
+ public static $defaultConfig = array(
+ 'use_ldap_for_authentication' => 1,
+ 'synchronize_users_after_login' => 1,
+ 'enable_synchronize_access_from_ldap' => 0,
+ 'new_user_default_sites_view_access' => '',
+ 'user_email_suffix' => '',
+ 'append_user_email_suffix_to_username' => 1,
+ 'required_member_of' => '',
+ 'required_member_of_field' => 'memberOf',
+ 'ldap_user_filter' => '',
+ 'ldap_user_id_field' => 'uid',
+ 'ldap_last_name_field' => 'sn',
+ 'ldap_first_name_field' => 'givenName',
+ 'ldap_alias_field' => 'cn',
+ 'ldap_mail_field' => 'mail',
+ 'ldap_password_field' => 'userPassword',
+ 'ldap_view_access_field' => 'view',
+ 'ldap_admin_access_field' => 'admin',
+ 'ldap_superuser_access_field' => 'superuser',
+ 'use_webserver_auth' => 0,
+ 'user_access_attribute_server_specification_delimiter' => ';',
+ 'user_access_attribute_server_separator' => ':',
+ 'instance_name' => '',
+ 'ldap_network_timeout' => Client::DEFAULT_TIMEOUT_SECS
+ );
+
+ // for backwards compatibility
+ public static $alternateOptionNames = array(
+ 'user_email_suffix' => array('usernameSuffix'),
+ 'required_member_of' => array('memberOf'),
+ 'ldap_user_filter' => array('filter'),
+ 'ldap_user_id_field' => array('userIdField'),
+ 'ldap_alias_field' => array('aliasField'),
+ 'ldap_mail_field' => array('mailField'),
+ 'use_webserver_auth' => array('useKerberos'),
+ );
+
+ /**
+ * Returns an INI option value that is stored in the `[LoginLdap]` config section.
+ *
+ * If alternate option names exist for the option name, they will be used as fallbacks.
+ *
+ * @param $optionName
+ * @return mixed
+ */
+ public static function getConfigOption($optionName)
+ {
+ return self::getConfigOptionFrom(PiwikConfig::getInstance()->LoginLdap, $optionName);
+ }
+
+ public static function getConfigOptionFrom($config, $optionName)
+ {
+ if (isset($config[$optionName])) {
+ return $config[$optionName];
+ } else if (isset(self::$alternateOptionNames[$optionName])) {
+ foreach (self::$alternateOptionNames[$optionName] as $alternateName) {
+ if (isset($config[$alternateName])) {
+ return $config[$alternateName];
+ }
+ }
+ return self::getDefaultConfigOptionValue($optionName);
+ } else {
+ return self::getDefaultConfigOptionValue($optionName);
+ }
+ }
+
+ public static function getDefaultConfigOptionValue($optionName)
+ {
+ return @self::$defaultConfig[$optionName];
+ }
+
+ public static function isAccessSynchronizationEnabled()
+ {
+ return self::getConfigOption('enable_synchronize_access_from_ldap');
+ }
+
+ public static function getDefaultSitesToGiveViewAccessTo()
+ {
+ return self::getConfigOption('new_user_default_sites_view_access');
+ }
+
+ public static function getRequiredMemberOf()
+ {
+ return self::getConfigOption('required_member_of');
+ }
+
+ public static function getRequiredMemberOfField()
+ {
+ return self::getConfigOption('required_member_of_field');
+ }
+
+ public static function getLdapUserFilter()
+ {
+ return self::getConfigOption('ldap_user_filter');
+ }
+
+ public static function getLdapUserIdField()
+ {
+ return self::getConfigOption('ldap_user_id_field');
+ }
+
+ public static function getLdapLastNameField()
+ {
+ return self::getConfigOption('ldap_last_name_field');
+ }
+
+ public static function getLdapFirstNameField()
+ {
+ return self::getConfigOption('ldap_first_name_field');
+ }
+
+ public static function getLdapAliasField()
+ {
+ return self::getConfigOption('ldap_alias_field');
+ }
+
+ public static function getLdapMailField()
+ {
+ return self::getConfigOption('ldap_mail_field');
+ }
+
+ public static function getLdapPasswordField()
+ {
+ return self::getConfigOption('ldap_password_field');
+ }
+
+ public static function getLdapUserEmailSuffix()
+ {
+ return self::getConfigOption('user_email_suffix');
+ }
+
+ public static function getLdapViewAccessField()
+ {
+ return self::getConfigOption('ldap_view_access_field');
+ }
+
+ public static function getLdapAdminAccessField()
+ {
+ return self::getConfigOption('ldap_admin_access_field');
+ }
+
+ public static function getSuperUserAccessField()
+ {
+ return self::getConfigOption('ldap_superuser_access_field');
+ }
+
+ public static function shouldUseWebServerAuthentication()
+ {
+ return self::getConfigOption('use_webserver_auth') == 1;
+ }
+
+ public static function getUserAccessAttributeServerSpecificationDelimiter()
+ {
+ return self::getConfigOption('user_access_attribute_server_specification_delimiter');
+ }
+
+ public static function getUserAccessAttributeServerSiteListSeparator()
+ {
+ return self::getConfigOption('user_access_attribute_server_separator');
+ }
+
+ public static function getDesignatedPiwikInstanceName()
+ {
+ return self::getConfigOption('instance_name');
+ }
+
+ public static function getUseLdapForAuthentication()
+ {
+ return self::getConfigOption('use_ldap_for_authentication') == 1;
+ }
+
+ public static function getShouldSynchronizeUsersAfterLogin()
+ {
+ return self::getConfigOption('synchronize_users_after_login') == 1;
+ }
+
+ public static function getLdapNetworkTimeout()
+ {
+ return self::getConfigOption('ldap_network_timeout');
+ }
+
+ public static function shouldAppendUserEmailSuffixToUsername()
+ {
+ return self::getConfigOption('append_user_email_suffix_to_username') == 1;
+ }
+
+ public static function getServerConfig($server)
+ {
+ $configName = 'LoginLdap_' . $server;
+ return PiwikConfig::getInstance()->__get($configName);
+ }
+
+ public static function getServerNameList()
+ {
+ return self::getConfigOption('servers');
+ }
+
+ /**
+ * Returns a list of {@link ServerInfo} instances describing the LDAP servers
+ * that should be connected to.
+ *
+ * @return ServerInfo[]
+ */
+ public static function getConfiguredLdapServers()
+ {
+ $serverNameList = self::getServerNameList();
+
+ if (empty($serverNameList)) {
+ $server = ServerInfo::makeFromOldConfig();
+ $serverHost = $server->getServerHostname();
+
+ if (empty($serverHost)) {
+ return array();
+ } else {
+ return array($server);
+ }
+ } else {
+ if (is_string($serverNameList)) {
+ $serverNameList = explode(',', $serverNameList);
+ }
+
+ $servers = array();
+ foreach ($serverNameList as $name) {
+ try {
+ $servers[] = ServerInfo::makeConfigured($name);
+ } catch (Exception $ex) {
+ /** @var LoggerInterface */
+ $logger = StaticContainer::get('Psr\Log\LoggerInterface');
+
+ $logger->debug("LoginLdap\\Config::{func}: LDAP server info '{name}' is configured incorrectly: {message}", array(
+ 'func' => __FUNCTION__,
+ 'name' => $name,
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+ }
+ return $servers;
+ }
+ }
+
+ public static function getPluginOptionValuesWithDefaults()
+ {
+ $result = self::$defaultConfig;
+ foreach ($result as $name => $ignore) {
+ $actualValue = Config::getConfigOption($name);
+
+ // special check for useKerberos which can be a string
+ if ($name == 'use_webserver_auth'
+ && $actualValue === 'false'
+ ) {
+ $actualValue = 0;
+ }
+
+ if (isset($actualValue)) {
+ $result[$name] = $actualValue;
+ }
+ }
+ return $result;
+ }
+
+ public static function savePluginOptions($config)
+ {
+ $loginLdap = PiwikConfig::getInstance()->LoginLdap;
+
+ foreach (self::$defaultConfig as $name => $value) {
+ if (isset($config[$name])) {
+ $loginLdap[$name] = $config[$name];
+ }
+ }
+
+ PiwikConfig::getInstance()->LoginLdap = $loginLdap;
+ PiwikConfig::getInstance()->forceSave();
+ }
+
+ public static function saveLdapServerConfigs($servers)
+ {
+ $serverNames = array();
+ foreach ($servers as $serverInfo) {
+ ServerInfo::saveServerConfig($serverInfo, $forceSave = false);
+
+ $serverNames[] = $serverInfo['name'];
+ }
+ PiwikConfig::getInstance()->LoginLdap['servers']= $serverNames;
+
+ PiwikConfig::getInstance()->forceSave();
+ }
+} \ No newline at end of file
diff --git a/Controller.php b/Controller.php
new file mode 100644
index 0000000..3902f4d
--- /dev/null
+++ b/Controller.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Exception;
+use Piwik\Nonce;
+use Piwik\Notification;
+use Piwik\Option;
+use Piwik\Piwik;
+use Piwik\Plugin\ControllerAdmin;
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugins\LoginLdap\Ldap\ServerInfo;
+use Piwik\Session;
+use Piwik\View;
+
+/**
+ * Login controller
+ *
+ * @package Login
+ */
+class Controller extends \Piwik\Plugins\Login\Controller
+{
+ /**
+ * @return string
+ */
+ public function admin()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+ $view = new View('@LoginLdap/index');
+
+ ControllerAdmin::setBasicVariablesAdminView($view);
+
+ if (!function_exists('ldap_connect')) {
+ $notification = new Notification(Piwik::translate('LoginLdap_LdapFunctionsMissing'));
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->type = Notification::TYPE_TRANSIENT;
+ $notification->flags = 0;
+ Notification\Manager::notify('LoginLdap_LdapFunctionsMissing', $notification);
+ }
+
+ $this->setBasicVariablesView($view);
+
+ $serverNames = Config::getServerNameList() ?: array();
+
+ $view->servers = array();
+ if (empty($serverNames)) {
+ try {
+ $serverInfo = ServerInfo::makeFromOldConfig()->getProperties();
+ $serverInfo['name'] = 'server';
+ $view->servers[] = $serverInfo;
+ } catch (Exception $ex) {
+ // ignore
+ }
+ } else {
+ foreach ($serverNames as $server) {
+ $serverConfig = Config::getServerConfig($server);
+ if (!empty($serverConfig)) {
+ $serverConfig['name'] = $server;
+ $view->servers[] = $serverConfig;
+ }
+ }
+ }
+
+ // remove password field
+ foreach ($view->servers as &$serverInfo) {
+ unset($serverInfo['admin_pass']);
+ }
+
+ $view->ldapConfig = Config::getPluginOptionValuesWithDefaults();
+
+ $view->isLoginControllerActivated = PluginManager::getInstance()->isPluginActivated('Login');
+
+ $view->updatedFromPre30 = Option::get('LoginLdap_updatedFromPre3_0');
+
+ return $view->render();
+ }
+} \ No newline at end of file
diff --git a/Ldap/Client.php b/Ldap/Client.php
new file mode 100644
index 0000000..fb0f31e
--- /dev/null
+++ b/Ldap/Client.php
@@ -0,0 +1,401 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\Ldap;
+
+use Exception;
+use Piwik\Container\StaticContainer;
+use Psr\Log\LoggerInterface;
+
+/**
+ * LDAP Client. Supports connecting to LDAP servers, binding to resource DNs and executing
+ * LDAP queries.
+ */
+class Client
+{
+ const DEFAULT_TIMEOUT_SECS = 15;
+
+ private static $initialBindErrorCodesToIgnore = array(
+ 7, // LDAP_AUTH_METHOD_NOT_SUPPORTED
+ 8, // LDAP_STRONG_AUTH_REQUIRED
+ 48, // LDAP_INAPPROPRIATE_AUTH
+ 49, // LDAP_INVALID_CREDENTIALS
+ 50, // LDAP_INSUFFICIENT_ACCESS
+ );
+
+ /**
+ * The LDAP connection resource. Set to the result of `ldap_connect`.
+ *
+ * @var resource
+ */
+ private $connectionResource;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Constructor.
+ *
+ * @param string|null $serverHostName The hostname of the LDAP server. If not null, an attempt
+ * to connect is made.
+ * @param int $port The server port to use.
+ * @throws Exception if a connection is attempted and it fails.
+ */
+ public function __construct($serverHostName = null, $port = ServerInfo::DEFAULT_LDAP_PORT, $timeout = self::DEFAULT_TIMEOUT_SECS,
+ LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+
+ if (!empty($serverHostName)) {
+ $this->connect($serverHostName, $port, $timeout);
+ }
+ }
+
+ /**
+ * Tries to connect to an LDAP server.
+ *
+ * If a connection is currently open, it is closed.
+ *
+ * All PHP errors triggered by ldap_* calls are wrapped in exceptions and thrown.
+ *
+ * @param string $serverHostName The hostname of the LDAP server.
+ * @param int $port The server port to use.
+ * @param int $timeout The timeout in seconds of the network connection.
+ * @throws Exception If an error occurs during the `ldap_connect` call or if there is a connection
+ * issue during the subsequent anonymous bind.
+ */
+ public function connect($serverHostName, $port = ServerInfo::DEFAULT_LDAP_PORT, $timeout = self::DEFAULT_TIMEOUT_SECS)
+ {
+ $this->closeIfCurrentlyOpen();
+
+ $this->logger->debug("Calling ldap_connect('{host}', {port})", array('host' => $serverHostName, 'port' => $port));
+
+ $this->connectionResource = ldap_connect($serverHostName, $port);
+
+ ldap_set_option($this->connectionResource, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_set_option($this->connectionResource, LDAP_OPT_REFERRALS, 0);
+ ldap_set_option($this->connectionResource, LDAP_OPT_NETWORK_TIMEOUT, $timeout);
+
+ $this->logger->debug("ldap_connect result is {result}", array('result' => $this->connectionResource));
+
+ // ldap_connect will not always try to connect to the server, so execute a bind
+ // to test the connection
+ try {
+ ldap_bind($this->connectionResource);
+
+ $this->logger->debug("anonymous ldap_bind call finished; connection ok");
+ } catch (Exception $ex) {
+ // if the error was due to a connection error, rethrow, otherwise ignore it
+ $errno = ldap_errno($this->connectionResource);
+
+ $this->logger->debug("anonymous ldap_bind returned error '{err}'", array('err' => $errno));
+
+ if (!in_array($errno, self::$initialBindErrorCodesToIgnore)) {
+ throw $ex;
+ }
+ }
+
+ if (!$this->isOpen()) { // sanity check
+ throw new Exception("sanity check failed: ldap_connect did not return a connection resource!");
+ }
+ }
+
+ /**
+ * Closes a currently open LDAP server connection.
+ *
+ * If a connection is not open, nothing is done.
+ *
+ * All PHP errors triggered by ldap_* calls are wrapped in exceptions and thrown.
+ *
+ * @throws Exception If an error occurs during the `ldap_close` call.
+ */
+ public function close()
+ {
+ if ($this->isOpen()) {
+ $this->doClose();
+ }
+ }
+
+ /**
+ * Binds to the LDAP server using a resource DN and a password.
+ *
+ * All PHP errors triggered by ldap_* calls are wrapped in exceptions and thrown.
+ *
+ * @param string $resourceDn The LDAP resource DN to use when binding.
+ * @param string $password The resource's associated password.
+ * @throws Exception If an error occurs during the `ldap_bind` call.
+ * @return bool
+ */
+ public function bind($resourceDn, $password)
+ {
+ $connectionResource = $this->connectionResource;
+
+ $this->logger->debug("Calling ldap_bind({conn}, '{dn}', <password[length={passlen}]>)", array(
+ 'conn' => $connectionResource,
+ 'dn' => $resourceDn,
+ 'passlen' => strlen($password)
+ ));
+
+ $result = ldap_bind($connectionResource, $resourceDn, $password);
+
+ $this->logger->debug("ldap_bind result is '{result}'", array('result' => (int)$result));
+
+ return $result;
+ }
+
+ /**
+ * Performs a search of LDAP entities on the currently bound LDAP connection and
+ * returns the result.
+ *
+ * All PHP errors triggered by ldap_* calls are wrapped in exceptions and thrown.
+ *
+ * @param string $baseDn The base DN to use.
+ * @param string $ldapFilter The LDAP filter string, ie, `"(&(...)(...))"`. This client allows you to use
+ * `"?"` placeholders in the string.
+ * @param array $filterBind Bind parameters for $ldapFilter.
+ * @param array $attributes The LDAP entry attributes to fetch. If empty, selects all of them.
+ * @return array|null The result of `ldap_get_entries` or null if `ldap_search` fails somehow.
+ * @throws Exception If an error occurs during the `ldap_search` or `ldap_get_entries` calls.
+ */
+ public function fetchAll($baseDn, $ldapFilter, $filterBind = array(), $attributes = array())
+ {
+ $ldapFilter = $this->bindFilterParameters($ldapFilter, $filterBind);
+
+ $searchResultResource = $this->initiateSearch($baseDn, $ldapFilter, $attributes);
+
+ if (!empty($searchResultResource)) {
+ $connectionResource = $this->connectionResource;
+
+ $this->logger->debug("Calling ldap_get_entries({conn}, {result})", array(
+ 'conn' => $connectionResource,
+ 'result' => $searchResultResource
+ ));
+
+ $ldapInfo = ldap_get_entries($connectionResource, $searchResultResource);
+
+ $this->logger->debug("ldap_get_entries result is {result}", array('result' => $ldapInfo === null ? 'null' : 'not null'));
+
+ return $this->transformLdapInfo($ldapInfo);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the count of LDAP entries that match a filter.
+ *
+ * All PHP errors triggered by ldap_* calls are wrapped in exceptions and thrown.
+ *
+ * @param string $baseDn The base DN to use.
+ * @param string $ldapFilter The LDAP filter string, ie, `"(&(...)(...))"`. This client allows you to use
+ * `"?"` placeholders in the string.
+ * @param array $filterBind Bind parameters for $ldapFilter.
+ * @return int The count of matched entries.
+ * @throws Exception If an error occurs during the `ldap_search` or `ldap_count_entries` calls, or if
+ * `ldap_search` returns null.
+ */
+ public function count($baseDn, $ldapFilter, $filterBind = array())
+ {
+ $ldapFilter = $this->bindFilterParameters($ldapFilter, $filterBind);
+
+ $searchResultResource = $this->initiateSearch($baseDn, $ldapFilter);
+
+ if (!empty($searchResultResource)) {
+ $connectionResource = $this->connectionResource;
+
+ $this->logger->debug("Calling ldap_count_entries({conn}, {result})", array(
+ 'conn' => $connectionResource,
+ 'result' => $searchResultResource
+ ));
+
+ $result = ldap_count_entries($connectionResource, $searchResultResource);
+
+ $this->logger->debug("ldap_count_entries returned {result}", array('result' => $result));
+
+ return $result;
+ } else {
+ $this->logger->warning("Unexpected error: ldap_search returned null, extra info: {err}", array('err' => ldap_error($this->connectionResource)));
+
+ throw new Exception("Unexpected error: ldap_search returned null.");
+ }
+ }
+
+ /**
+ * Returns true if there is currently an open connection being managed, false if otherwise.
+ *
+ * @return bool
+ */
+ public function isOpen()
+ {
+ return $this->connectionResource !== null
+ && $this->connectionResource !== false;
+ }
+
+ private function doClose()
+ {
+ $connectionResource = $this->connectionResource;
+
+ $this->logger->debug("Calling ldap_close({conn})", array('conn' => $connectionResource));
+
+ $result = ldap_close($connectionResource);
+
+ $this->logger->debug("ldap_close returned {result}", array('result' => $result ? 'true' : 'false'));
+
+ return$result;
+ }
+
+ private function closeIfCurrentlyOpen()
+ {
+ if ($this->isOpen()) {
+ $this->doClose();
+
+ $this->connectionResource = null;
+ }
+ }
+
+ private function bindFilterParameters($ldapFilter, $bind)
+ {
+ $idx = 0;
+ return preg_replace_callback("/(?<!\\\\)[?]/", function ($matches) use (&$idx, $bind) {
+ if (!isset($bind[$idx])) {
+ return "?";
+ }
+
+ $result = Client::escapeFilterParameter($bind[$idx]);
+
+ ++$idx;
+
+ return $result;
+ }, $ldapFilter);
+ }
+
+ /**
+ * Converts information returned by `ldap_search` into a normal PHP array.
+ *
+ * `ldap_search` returns results like this:
+ *
+ * array(
+ * 'count' => '2',
+ * '0' => array(
+ * 'count' => 1,
+ * 'cn' => array(
+ * 'count' => 1,
+ * '0' => 'value'
+ * )
+ * ),
+ * '1' => array(
+ * 'count' => 1,
+ * 'objectclass => array(
+ * 'count' => 2,
+ * '0' => 'inetOrgPerson',
+ * '1' => 'top'
+ * )
+ * )
+ * )
+ *
+ * This method will convert that to:
+ *
+ * array(
+ * 'cn' => 'value',
+ * 'objectclass' => array('inetOrgPerson', 'top')
+ * )
+ *
+ */
+ private function transformLdapInfo($ldapInfo)
+ {
+ $result = array();
+
+ $processedKeys = array('count');
+
+ $count = @$ldapInfo['count'];
+ for ($i = 0; $i < $count; ++$i) {
+ if (!isset($ldapInfo[$i])) {
+ continue;
+ }
+
+ $value = $ldapInfo[$i];
+
+ if (is_array($value)) { // index is for array, ie 0 => array(...)
+ $result[$i] = $this->transformLdapInfo($value);
+ } else if (!is_numeric($value)
+ && isset($ldapInfo[$value])
+ ) { // index is for name of attribute, ie 0 => 'cn', 'cn' => array(...)
+ $key = strtolower($value);
+
+ if (is_array($ldapInfo[$value])) {
+ $result[$key] = $this->transformLdapInfo($ldapInfo[$value]);
+
+ if (count($result[$key]) == 1) {
+ $result[$key] = reset($result[$key]);
+ }
+ } else {
+ $result[$key] = $ldapInfo[$value];
+ }
+
+ $processedKeys[] = $key;
+ } else { // index is for attribute value
+ $result[$i] = $value;
+ }
+ }
+
+ // process keys that have no associated index (ie, a 'dn' => that has no N => 'dn')
+ foreach ($ldapInfo as $key => $value) {
+ if (is_int($key)) {
+ continue;
+ }
+
+ $key = strtolower($key);
+ if (in_array($key, $processedKeys)) {
+ continue;
+ }
+
+ $result[$key] = $value;
+ }
+
+ return $result;
+ }
+
+ private function initiateSearch($baseDn, $ldapFilter, $attributes = array())
+ {
+ $connectionResource = $this->connectionResource;
+
+ $this->logger->debug("Calling ldap_search({conn}, '{dn}', '{filter}')", array(
+ 'conn' => $connectionResource,
+ 'dn' => $baseDn,
+ 'filter' => $ldapFilter
+ ));
+
+ $result = ldap_search($connectionResource, $baseDn, $ldapFilter, $attributes);
+
+ $this->logger->debug("ldap_search result is {result}", array('result' => $result));
+
+ return $result;
+ }
+
+ /**
+ * Escapes an LDAP string for use in a filter.
+ *
+ * @param mixed $value The value that should be inserted into an LDAP filter. Converted to
+ * a string before being escaped.
+ * @return string The escaped string.
+ */
+ public static function escapeFilterParameter($value)
+ {
+ $value = (string) $value;
+
+ if (function_exists('ldap_escape')) { // available in PHP 5.6
+ return ldap_escape($value, $ignoreChars = "", LDAP_ESCAPE_FILTER);
+ } else {
+ return preg_replace_callback("/[*()\\\\]/", function ($matches) { // replace special filter characters
+ return "\\" . bin2hex($matches[0]);
+ }, $value);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ldap/Exceptions/ConnectionException.php b/Ldap/Exceptions/ConnectionException.php
new file mode 100644
index 0000000..b3ff594
--- /dev/null
+++ b/Ldap/Exceptions/ConnectionException.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\Ldap\Exceptions;
+
+use RuntimeException;
+
+/**
+ * Custom exception that can be thrown when connection to one or more LDAP servers fails.
+ */
+class ConnectionException extends RuntimeException
+{
+} \ No newline at end of file
diff --git a/Ldap/ServerInfo.php b/Ldap/ServerInfo.php
new file mode 100644
index 0000000..adf4c2e
--- /dev/null
+++ b/Ldap/ServerInfo.php
@@ -0,0 +1,308 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Ldap;
+
+use Exception;
+use Piwik\Plugins\LoginLdap\Config;
+
+/**
+ * Describes an LDAP server LoginLdap can connect to.
+ */
+class ServerInfo
+{
+ const DEFAULT_LDAP_PORT = 389;
+
+ /**
+ * The LDAP server hostname.
+ *
+ * @var string
+ */
+ private $serverHostname;
+
+ /**
+ * The port to use when connecting to the LDAP server.
+ *
+ * @var int
+ */
+ private $serverPort;
+
+ /**
+ * The base DN to use when searching the LDAP server. Determines which specific
+ * LDAP database is searched.
+ *
+ * @var string
+ */
+ private $baseDn;
+
+ /**
+ * The 'admin' LDAP user to use when authenticating. This user must have read
+ * access to other users so we can search for the person attempting login.
+ *
+ * @var string
+ */
+ private $adminUsername;
+
+ /**
+ * The password to use when binding w/ the 'admin' LDAP user.
+ *
+ * @var string
+ */
+ private $adminPassword;
+
+ /**
+ * Constructor.
+ *
+ * @param string $serverHostname See {@link $serverHostname}.
+ * @param string $baseDn See {@link $baseDn}.
+ * @param int $serverPort See {@link $serverPort}.
+ * @param string|null $adminUsername See {@link $adminUsername}.
+ * @param string|null $adminPassword See {@link $adminPassword}.
+ */
+ public function __construct($serverHostname, $baseDn, $serverPort = self::DEFAULT_LDAP_PORT, $adminUsername = null,
+ $adminPassword = null)
+ {
+ $this->serverHostname = $serverHostname;
+ $this->baseDn = $baseDn;
+ $this->serverPort = $serverPort;
+ $this->adminUsername = $adminUsername;
+ $this->adminPassword = $adminPassword;
+ }
+
+ /**
+ * Gets the {@link $serverHostname} property.
+ *
+ * @return string
+ */
+ public function getServerHostname() {
+ return $this->serverHostname;
+ }
+
+ /**
+ * Sets the {@link $serverHostname} property.
+ *
+ * @param string $serverHostname
+ */
+ public function setServerHostname($serverHostname) {
+ $this->serverHostname = $serverHostname;
+ }
+
+ /**
+ * Gets the {@link $serverPort} property.
+ *
+ * @return int
+ */
+ public function getServerPort() {
+ return $this->serverPort;
+ }
+
+ /**
+ * Sets the {@link $serverPort} property.
+ *
+ * @param int $serverPort
+ */
+ public function setServerPort($serverPort) {
+ $this->serverPort = $serverPort;
+ }
+
+ /**
+ * Gets the {@link $baseDn} property.
+ *
+ * @return string
+ */
+ public function getBaseDn() {
+ return $this->baseDn;
+ }
+
+ /**
+ * Sets the {@link $baseDn} property.
+ *
+ * @param string $baseDn
+ */
+ public function setBaseDn($baseDn) {
+ $this->baseDn = $baseDn;
+ }
+
+ /**
+ * Gets the {@link $adminUsername} property.
+ *
+ * @return string
+ */
+ public function getAdminUsername() {
+ return $this->adminUsername;
+ }
+
+ /**
+ * Sets the {@link $adminUsername} property.
+ *
+ * @param string $adminUsername
+ */
+ public function setAdminUsername($adminUsername) {
+ $this->adminUsername = $adminUsername;
+ }
+
+ /**
+ * Gets the {@link $adminPassword} property.
+ *
+ * @return string
+ */
+ public function getAdminPassword() {
+ return $this->adminPassword;
+ }
+
+ /**
+ * Sets the {@link $adminPassword} property.
+ *
+ * @param string $adminPassword
+ */
+ public function setAdminPassword($adminPassword) {
+ $this->adminPassword = $adminPassword;
+ }
+
+ /**
+ * Returns ServerInfo properties as an array. Array keys are the same keys used in INI
+ * config.
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ return array(
+ 'hostname' => $this->getServerHostname(),
+ 'port' => $this->getServerPort(),
+ 'base_dn' => $this->getBaseDn(),
+ 'admin_user' => $this->getAdminUsername(),
+ 'admin_pass' => $this->getAdminPassword()
+ );
+ }
+
+ /**
+ * Creates a ServerInfo instance from an array of old LoginLdap config data.
+ *
+ * @return ServerInfo
+ */
+ public static function makeFromOldConfig()
+ {
+ $hostname = Config::getConfigOption('serverUrl');
+ $baseDn = Config::getConfigOption('baseDn');
+
+ $result = new ServerInfo($hostname, $baseDn);
+
+ $ldapPort = Config::getConfigOption('ldapPort');
+ if (!empty($ldapPort)) {
+ $result->setServerPort((int) $ldapPort);
+ }
+
+ $result->setAdminUsername(Config::getConfigOption('adminUser'));
+ $result->setAdminPassword(Config::getConfigOption('adminPass'));
+
+ return $result;
+ }
+
+ /**
+ * Returns a ServerInfo instance created using options in an INI config section.
+ * The INI config section's name is determined by prefixing `'LoginLdap_'` to the
+ * server name.
+ *
+ * The INI config section can have the following information:
+ *
+ * - **hostname** _(Required)_ The server's hostname.
+ * - **base_dn** _(Required)_ The base DN to use with this server.
+ * - **port** The port to use when connecting to the server.
+ * - **admin_user** The name of an admin user that has read access to other users.
+ * - **admin_pass** The password to use when binding with the admin user.
+ *
+ * @param string $name The name of the LDAP server in config. This value can be
+ * used in the `[LoginLdap] servers[] = ` config option to
+ * add an LDAP server to the list of servers LoginLdap will
+ * connect to.
+ * @return ServerInfo
+ * @throws Exception if the LDAP server config cannot be found or is missing
+ * required information.
+ */
+ public static function makeConfigured($name)
+ {
+ $config = Config::getServerConfig($name);
+
+ if (empty($config)) {
+ throw new Exception("No configuration section [$name] found.");
+ }
+
+ if (empty($config['hostname'])) {
+ throw new Exception("Required config option 'hostname' not found in [$name] section.");
+ }
+
+ if (empty($config['base_dn'])) {
+ throw new Exception("Required config option 'base_dn' not found in [$name] section.");
+ }
+
+ $hostname = $config['hostname'];
+ $baseDn = $config['base_dn'];
+
+ $result = new ServerInfo($hostname, $baseDn);
+
+ $port = $config['port'];
+ if (!empty($port)) {
+ $result->setServerPort((int) $port);
+ }
+
+ $adminUser = $config['admin_user'];
+ if (!empty($adminUser)) {
+ $result->setAdminUsername($adminUser);
+ }
+
+ $adminPass = $config['admin_pass'];
+ if (!empty($adminPass)) {
+ $result->setAdminPassword($adminPass);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sets an INI config section using an array of LDAP server info.
+ *
+ * @param string[] $serverInfo
+ * @param bool $forceSave If true, configuration changes are saved before this method exits.
+ * @throws Exception if hostname or base_dn are missing from $serverInfo.
+ */
+ public static function saveServerConfig($serverInfo, $forceSave = true)
+ {
+ if (empty($serverInfo['name'])) {
+ throw new Exception("Server info array has no name!");
+ }
+
+ if (empty($serverInfo['hostname'])) {
+ throw new Exception("'hostname' property is required for server '{$serverInfo['name']}'.");
+ }
+
+ if (empty($serverInfo['base_dn'])) {
+ throw new Exception("'base_dn' property is required for server '{$serverInfo['name']}'.");
+ }
+
+ $config = \Piwik\Config::getInstance();
+
+ $configSectionName = 'LoginLdap_' . $serverInfo['name'];
+ $existingServerInfo = $config->__get($configSectionName);
+
+ $existingPassword = !empty($existingServerInfo['admin_pass']) ? $existingServerInfo['admin_pass'] : "";
+ $passwordToSet = !empty($serverInfo['admin_pass']) ? $serverInfo['admin_pass'] : $existingPassword;
+ $configSection = array(
+ 'hostname' => $serverInfo['hostname'],
+ 'port' => @$serverInfo['port'],
+ 'base_dn' => $serverInfo['base_dn'],
+ 'admin_user' => @$serverInfo['admin_user'],
+ 'admin_pass' => $passwordToSet,
+ );
+
+ $config->__set($configSectionName, $configSection);
+
+ if ($forceSave) {
+ $config->forceSave();
+ }
+ }
+} \ No newline at end of file
diff --git a/LdapInterop/UserAccessAttributeParser.php b/LdapInterop/UserAccessAttributeParser.php
new file mode 100644
index 0000000..86e3ca7
--- /dev/null
+++ b/LdapInterop/UserAccessAttributeParser.php
@@ -0,0 +1,405 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\LdapInterop;
+
+use Piwik\Access;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Site;
+use Piwik\Url;
+use Piwik\SettingsPiwik;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Parses the values of LDAP attributes that describe an LDAP user's Piwik access.
+ *
+ * ### Access Attribute Format
+ *
+ * Access attributes can have different formats, the simplest is simply a list of IDs
+ * or `'all'`, eg:
+ *
+ * view: 1,2,3
+ * admin: all
+ *
+ * ### Managing Multiple Piwik Instances
+ *
+ * If the LDAP server in question manages access for only a single Piwik instance, this
+ * will suffice. To support multiple Piwik instances, it is allowed to identify the
+ * server instance within the attributes, eg:
+ *
+ * view: piwikServerA:1,2,3
+ * view: piwikServerB:1,2,3
+ * admin: piwikServerA:all;piwikServerB:2,3
+ *
+ * In this example, the user is granted view access for sites 1, 2 & 3 for Piwik instance
+ * 'A' and Piwik instance 'B', and is granted admin access for all sites in Piwik instance 'A',
+ * but only sites 2 & 3 in Piwik instance 'B'.
+ *
+ * As demonstrated above, instance ID/site list pairs (ie, `"piwikServerA:1,2,3"`) can be in
+ * multiple values, or in a single value separated by a delimiter.
+ *
+ * The seaparator used to split instance ID/site list pairs and the delimiter used to
+ * separate pair from other pairs can both be customized through INI config options.
+ *
+ * ### Identifying Piwik Instances
+ *
+ * In the above example, Piwik instances are identified by a special name, ie,
+ * `"piwikServerA"` or `"piwikServerB"`. By default, however, instances are identified by
+ * the instance's host, port and url. For example:
+ *
+ * view: piwikA.myhost.com/path/to/piwik:1,2,3
+ * view: piwikB.myhost.com/path/to/piwik:all
+ * admin: piwikC.com:all
+ * superuser: piwikC.com;piwikD.com
+ *
+ * If you want to use a specific name, you would have to set the `[LoginLdap] instance_name`
+ * INI config option for each of your Piwik instances.
+ *
+ * _Note: If identifying by URLs with port values, the `[LoginLdap] user\_access\_attribute\_server\_separator`
+ * config option should be set to something other than `':'`._
+ *
+ * ### Access Attribute Flexibility
+ *
+ * In order to make error conditions as rare as possible, this parser has been coded
+ * to be flexible when identifying instance IDs. Any malformed looking access values are
+ * logged with at least DEBUG level.
+ */
+class UserAccessAttributeParser
+{
+ /**
+ * The delimiter that separates individual instance ID/site list pairs from other pairs.
+ *
+ * For example, if `'#'` is used, the access attribute will be expected to be like:
+ *
+ * piwikServerA:1#piwikServerB:2#piwikServerC:3
+ *
+ * @var string
+ */
+ private $serverSpecificationDelimiter = ';';
+
+ /**
+ * The separator used to separate instance IDs from site ID lists.
+ *
+ * For example, if `'#'` is used, the access attribute will be expected be like:
+ *
+ * piwikServerA#1;piwikServerB#2,3;piwikServerC#3
+ *
+ * @var string
+ */
+ private $serverIdsSeparator = ':';
+
+ /**
+ * A special name for this Piwik instance. If not null, we check if a specification in
+ * an LDAP attribute value applies to this instance if the instance ID contains this value.
+ *
+ * If null, the instance ID is expected to be this Piwik instance's URL.
+ *
+ * @var string
+ */
+ private $thisPiwikInstanceName = null;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Parses an LDAP access attribute value and returns the list of site IDs that apply to
+ * this specific Piwik instance.
+ *
+ * @var string $attributeValue eg `"piwikServerA:1,2,3;piwikServerB:4,5,6"`.
+ * @return array
+ */
+ public function getSiteIdsFromAccessAttribute($attributeValue)
+ {
+ $result = array();
+
+ $instanceSpecs = explode($this->serverSpecificationDelimiter, $attributeValue);
+ foreach ($instanceSpecs as $spec) {
+ list($instanceId, $sitesSpec) = $this->getInstanceIdAndSitesFromSpec($spec);
+ if ($this->isInstanceIdForThisInstance($instanceId)) {
+ $result = array_merge($result, $this->getSitesFromSitesList($sitesSpec));
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns true if an LDAP access attribute value marks a user as a superuser.
+ *
+ * The superuser attribute doesn't need to have a site list so it just contains
+ * a list of instances.
+ */
+ public function getSuperUserAccessFromSuperUserAttribute($attributeValue)
+ {
+ $attributeValue = trim($attributeValue);
+
+ if ($attributeValue == 1
+ || strtolower($attributeValue) == 'true'
+ || empty($attributeValue)
+ ) { // special case when not managing multiple Piwik instances
+ return true;
+ }
+
+ $instanceIds = $this->getSuperUserInstancesFromAttribute($attributeValue);
+ foreach ($instanceIds as $instanceId) {
+ if ($this->isInstanceIdForThisInstance($instanceId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the {@link $serverSpecificationDelimiter} property value.
+ *
+ * @return string
+ */
+ public function getServerSpecificationDelimiter()
+ {
+ return $this->serverSpecificationDelimiter;
+ }
+
+ /**
+ * Sets the {@link $serverSpecificationDelimiter} property.
+ *
+ * @param string $serverSpecificationDelimiter
+ */
+ public function setServerSpecificationDelimiter($serverSpecificationDelimiter)
+ {
+ $this->serverSpecificationDelimiter = $serverSpecificationDelimiter;
+ }
+
+ /**
+ * Returns the {@link $serverIdsSeparator} property value.
+ *
+ * @return string
+ */
+ public function getServerIdsSeparator()
+ {
+ return $this->serverIdsSeparator;
+ }
+
+ /**
+ * Sets the {@link $serverIdsSeparator} property value.
+ *
+ * @param string $serverIdsSeparator
+ */
+ public function setServerIdsSeparator($serverIdsSeparator)
+ {
+ $this->serverIdsSeparator = $serverIdsSeparator;
+ }
+
+ /**
+ * Returns the {@link $thisPiwikInstanceName} property value.
+ *
+ * @return string
+ */
+ public function getThisPiwikInstanceName()
+ {
+ return $this->thisPiwikInstanceName;
+ }
+
+ /**
+ * Sets the {@link $thisPiwikInstanceName} property value.
+ *
+ * @param string $thisPiwikInstanceName
+ */
+ public function setThisPiwikInstanceName($thisPiwikInstanceName)
+ {
+ $this->thisPiwikInstanceName = $thisPiwikInstanceName;
+ }
+
+ /**
+ * Returns the instance ID and list of sites from an instance ID/sites list pair.
+ *
+ * @param string $spec eg, `"piwikServerA:1,2,3"`
+ * @return string[] contains two string elements
+ */
+ protected function getInstanceIdAndSitesFromSpec($spec)
+ {
+ $parts = explode($this->serverIdsSeparator, $spec);
+
+ if (count($parts) == 1) { // there is no instanceId
+ $parts = array(null, $parts[0]);
+ } else if (count($parts) >= 2) { // malformed server access specification
+ $this->logger->debug("UserAccessAttributeParser::{func}: Improper server specification in LDAP access attribute: '{value}'",
+ array('func' => __FUNCTION__, 'value' => $spec));
+
+ $parts = array($parts[0], $parts[1]);
+ }
+
+ return array_map('trim', $parts);
+ }
+
+ /**
+ * Returns true if an instance ID string found in LDAP refers to this instance or not.
+ *
+ * If not instance ID is specified, will always return `true`.
+ *
+ * @param string $instanceId eg, `"piwikServerA"` or `"piwikA.mysite.com"`
+ * @return bool
+ */
+ protected function isInstanceIdForThisInstance($instanceId)
+ {
+ if (empty($instanceId)) {
+ return true;
+ }
+
+ if ($this->thisPiwikInstanceName === null) {
+ $result = $this->isUrlThisInstanceUrl($instanceId);
+ } else {
+ preg_match("/\\b" . preg_quote($this->thisPiwikInstanceName) . "\\b/", $instanceId, $matches);
+
+ if (empty($matches)) {
+ $result = false;
+ } else {
+ if (strlen($matches[0]) != strlen($instanceId)) {
+ $this->logger->debug("UserAccessAttributeParser::{func}: Found extra characters in Piwik instance ID. Whole ID entry = {id}.",
+ array('func' => __FUNCTION__, 'id' => $instanceId));
+ }
+
+ $result = true;
+ }
+ }
+
+ if ($result) {
+ $this->logger->debug("UserAccessAttributeParser::{func}: Matched this instance with '{id}'.", array(
+ 'func' => __FUNCTION__,
+ 'id' => $instanceId
+ ));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns list of int site IDs from site list found in LDAP.
+ *
+ * @param string $sitesSpec eg, `"1,2,3"` or `"all"`
+ * @return int[]
+ */
+ protected function getSitesFromSitesList($sitesSpec)
+ {
+ return Access::doAsSuperUser(function () use ($sitesSpec) {
+ return Site::getIdSitesFromIdSitesString($sitesSpec);
+ });
+ }
+
+ /**
+ * Returns the list of instance IDs in a superuser access attribute value.
+ *
+ * @return string[]
+ */
+ protected function getSuperUserInstancesFromAttribute($attributeValue)
+ {
+ $delimiters = $this->serverIdsSeparator . $this->serverSpecificationDelimiter;
+ $result = preg_split("/[" . preg_quote($delimiters) . "]/", $attributeValue);
+ return array_map('trim', $result);
+ }
+
+ /**
+ * Returns true if the supplied instance ID refers to this Piwik instance, false if otherwise.
+ * Assumes the instance ID is the base URL to the Piwik instance.
+ *
+ * @param string $instanceIdUrl
+ * @return bool
+ */
+ protected function isUrlThisInstanceUrl($instanceIdUrl)
+ {
+ $thisPiwikUrl = SettingsPiwik::getPiwikUrl();
+ $thisPiwikUrl = $this->getNormalizedUrl($thisPiwikUrl, $isThisPiwikUrl = true);
+
+ $instanceIdUrl = $this->getNormalizedUrl($instanceIdUrl);
+
+ return $thisPiwikUrl == $instanceIdUrl;
+ }
+
+ private function getNormalizedUrl($url, $isThisPiwikUrl = false)
+ {
+ $parsed = @parse_url($url);
+ if (empty($parsed)) {
+ if ($isThisPiwikUrl) {
+ $this->logger->warning("UserAccessAttributeParser::{func}: Invalid Piwik URL found for this instance '{url}'.",
+ array('func' => __FUNCTION__, 'url' => $url));
+ } else {
+ $this->logger->debug("UserAccessAttributeParser::{func}: Invalid instance ID URL found '{url}'.",
+ array('func' => __FUNCTION__, 'url' => $url));
+ }
+
+ return false;
+ }
+
+ if (empty($parsed['scheme'])
+ && empty($parsed['host'])
+ ) { // parse_url will consider www.example.com the path if there is no protocol
+ $url = 'http://' . $url;
+ $parsed = @parse_url($url);
+ }
+
+ if (empty($parsed['host'])) {
+ $this->logger->debug("UserAccessAttributeParser::{func}: Found strange URL - '{url}'.", array(
+ 'func' => __FUNCTION__,
+ 'url' => $url
+ ));
+ }
+
+ if (!isset($parsed['port'])) {
+ $parsed['port'] = 80;
+ }
+
+ if (substr(@$parsed['path'], -1) !== '/') {
+ $parsed['path'] = @$parsed['path'] . '/';
+ }
+
+ return $parsed['host'] . ':' . $parsed['port'] . $parsed['path'];
+ }
+
+ /**
+ * Creates a UserAccessAttributeParser instance using INI configuration.
+ *
+ * @return UserAccessAttributeParser
+ */
+ public static function makeConfigured()
+ {
+ $result = new UserAccessAttributeParser();
+
+ $serverSpecificationDelimiter = Config::getUserAccessAttributeServerSpecificationDelimiter();
+ if (!empty($serverSpecificationDelimiter)) {
+ $result->setServerSpecificationDelimiter($serverSpecificationDelimiter);
+ }
+
+ $serverListSeparator = Config::getUserAccessAttributeServerSiteListSeparator();
+ if (!empty($serverListSeparator)) {
+ $result->setServerIdsSeparator($serverListSeparator);
+ }
+
+ $thisPiwikInstanceName = Config::getDesignatedPiwikInstanceName();
+ if (!empty($thisPiwikInstanceName)) {
+ $result->setThisPiwikInstanceName($thisPiwikInstanceName);
+ } else {
+ if ($result->getServerIdsSeparator() == ':') {
+ // TODO: remove this warning and move it to the settings page.
+ /** @var LoggerInterface $logger */
+ $logger = StaticContainer::get('Psr\Log\LoggerInterface');
+ $logger->info("UserAttributesParser::{func}: Configured with no instance name so matching by URL, but server/site IDs"
+ . " separator set to special ':' character. This character may show up in URLs in LDAP, which will "
+ . "cause problems. We recommend you use a character not often found in URLs, such as '|'.",
+ array('func' => __FUNCTION__));
+ }
+ }
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/LdapInterop/UserAccessMapper.php b/LdapInterop/UserAccessMapper.php
new file mode 100644
index 0000000..2116fd3
--- /dev/null
+++ b/LdapInterop/UserAccessMapper.php
@@ -0,0 +1,318 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\LdapInterop;
+
+use Piwik\Access;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Uses custom LDAP attributes to determine an LDAP user's Piwik permissions
+ * (ie, access to what sites and level of access).
+ *
+ * Note: This class does not set user access in the DB, it only determines what
+ * an LDAP user's access should be.
+ *
+ * See {@link UserSynchronizer} for more information on LDAP user synchronization.
+ *
+ * ### Custom LDAP Attributes
+ *
+ * LDAP has no knowledge of Piwik, so the attributes used by this class to determine
+ * Piwik access levels are non-standard. The implications of this are different
+ * for each LDAP server implementation, but it means if you try to just add
+ * a **view** or **superuser** attribute to an LDAP entry, it will probably fail.
+ *
+ * For OpenLDAP, you will have to modify the schema to allow these attributes.
+ */
+class UserAccessMapper
+{
+ /**
+ * The name of the LDAP attribute that holds the list of sites the user has
+ * view access to.
+ *
+ * @var string
+ */
+ private $viewAttributeName = 'view';
+
+ /**
+ * The name of the LDAP attribute that holds the list of sites the user has
+ * superuser access to.
+ *
+ * @var string
+ */
+ private $adminAttributeName = 'admin';
+
+ /**
+ * The name of the LDAP attribute that marks a user as a superuser. If the attribute
+ * is present but set to nothing (ie, `superuser: `) it will still cause the user to
+ * be a super user.
+ *
+ * @var string
+ */
+ private $superuserAttributeName = 'superuser';
+
+ /**
+ * The UserAccessAttributeParser instance used to the values of LDAP attributes that
+ * describe Piwik user access.
+ *
+ * @var UserAccessAttributeParser
+ */
+ private $userAccessAttributeParser;
+
+ /**
+ * Cache for all site IDs. Set once by {@link getAllSites()}.
+ *
+ * Maps int site IDs w/ unspecified data.
+ *
+ * @var array
+ */
+ private $allSites = null;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Returns an array describing an LDAP user's access to Piwik sites.
+ *
+ * The array will either mark the user as a superuser, in which case it will look like
+ * this:
+ *
+ * array('superuser' => true)
+ *
+ * Or it will map user access levels with lists of site IDs, for example:
+ *
+ * array(
+ * 'view' => array(1,2,3),
+ * 'admin' => array(3,4,5)
+ * )
+ *
+ * @param string[] $ldapUser The LDAP entity information.
+ * @return array
+ */
+ public function getPiwikUserAccessForLdapUser($ldapUser)
+ {
+ // if the user is a superuser, we don't need to check the other attributes
+ if ($this->isSuperUserAccessGrantedForLdapUser($ldapUser)) {
+ $this->logger->debug("UserAccessMapper::{func}: user '{user}' found to be superuser", array(
+ 'func' => __FUNCTION__,
+ 'user' => array_keys($ldapUser)
+ ));
+
+ return array('superuser' => true);
+ }
+
+ $sitesByAccess = array();
+
+ if (!empty($ldapUser[$this->viewAttributeName])) {
+ $this->addSiteAccess($sitesByAccess, 'view', $ldapUser[$this->viewAttributeName]);
+ }
+
+ if (!empty($ldapUser[$this->adminAttributeName])) {
+ $this->addSiteAccess($sitesByAccess, 'admin', $ldapUser[$this->adminAttributeName]);
+ }
+
+ $accessBySite = array();
+ foreach ($sitesByAccess as $site => $access) {
+ $accessBySite[$access][] = $site;
+ }
+ return $accessBySite;
+ }
+
+ /**
+ * Returns the {@link $viewAttributeName} property.
+ *
+ * @return string
+ */
+ public function getViewAttributeName()
+ {
+ return $this->viewAttributeName;
+ }
+
+ /**
+ * Sets the {@link $viewAttributeName} property.
+ *
+ * @param string $viewAttributeName
+ */
+ public function setViewAttributeName($viewAttributeName)
+ {
+ $this->viewAttributeName = strtolower($viewAttributeName);
+ }
+
+ /**
+ * Returns the {@link $viewAttributeName} property.
+ *
+ * @return string
+ */
+ public function getAdminAttributeName()
+ {
+ return $this->adminAttributeName;
+ }
+
+ /**
+ * Sets the {@link $viewAttributeName} property.
+ *
+ * @param string $adminAttributeName
+ */
+ public function setAdminAttributeName($adminAttributeName)
+ {
+ $this->adminAttributeName = strtolower($adminAttributeName);
+ }
+
+ /**
+ * Returns the {@link $superuserAttributeName} property.
+ *
+ * @return string
+ */
+ public function getSuperuserAttributeName()
+ {
+ return $this->superuserAttributeName;
+ }
+
+ /**
+ * Sets the {@link $superuserAttributeName} property.
+ *
+ * @param string $superuserAttributeName
+ */
+ public function setSuperuserAttributeName($superuserAttributeName)
+ {
+ $this->superuserAttributeName = strtolower($superuserAttributeName);
+ }
+
+ /**
+ * Returns the {@link $userAccessAttributeParser} property.
+ *
+ * @return UserAccessAttributeParser
+ */
+ public function getUserAccessAttributeParser()
+ {
+ return $this->userAccessAttributeParser;
+ }
+
+ /**
+ * Sets the {@link $userAccessAttributeParser} property.
+ *
+ * @param UserAccessAttributeParser $userAccessAttributeParser
+ */
+ public function setUserAccessAttributeParser($userAccessAttributeParser)
+ {
+ $this->userAccessAttributeParser = $userAccessAttributeParser;
+ }
+
+ private function addSiteAccess(&$sitesByAccess, $accessLevel, $viewAttributeValues)
+ {
+ if (!is_array($viewAttributeValues)) {
+ $viewAttributeValues = array($viewAttributeValues);
+ }
+
+ $this->logger->debug("UserAccessMapper::{func}(): attribute value for {accessLevel} access is {values}", array(
+ 'func' => __FUNCTION__,
+ 'accessLevel' => $accessLevel,
+ 'values' => $viewAttributeValues
+ ));
+
+ $siteIds = array();
+
+ $attributeParser = $this->userAccessAttributeParser;
+ Access::doAsSuperUser(function () use (&$siteIds, $viewAttributeValues, $attributeParser) {
+ foreach ($viewAttributeValues as $value) {
+ $siteIds = array_merge($siteIds, $attributeParser->getSiteIdsFromAccessAttribute($value));
+ }
+ });
+
+ $this->logger->debug("UserAccessMapper::{func}(): adding {accessLevel} access for sites {sites}", array(
+ 'func' => __FUNCTION__,
+ 'accessLevel' => $accessLevel,
+ 'sites' => $siteIds
+ ));
+
+ $allSitesSet = $this->getSetOfAllSites();
+ foreach ($siteIds as $idSite) {
+ if (!isset($allSitesSet[$idSite])) {
+ $this->logger->debug("UserAccessMapper::{func}(): site [ id = {id} ] does not exist, ignoring", array(
+ 'func' => __FUNCTION__,
+ 'id' => $idSite
+ ));
+
+ continue;
+ }
+
+ $sitesByAccess[$idSite] = $accessLevel;
+ }
+ }
+
+ private function getSetOfAllSites()
+ {
+ if ($this->allSites === null) {
+ $this->allSites = array_flip(Access::doAsSuperUser(function () {
+ return SitesManagerAPI::getInstance()->getSitesIdWithAtLeastViewAccess();
+ }));
+ }
+
+ return $this->allSites;
+ }
+
+ private function isSuperUserAccessGrantedForLdapUser($ldapUser)
+ {
+ if (!array_key_exists($this->superuserAttributeName, $ldapUser)) {
+ return false;
+ }
+
+ $attributeValue = $ldapUser[$this->superuserAttributeName];
+ if (!is_array($attributeValue)) {
+ $attributeValue = array($attributeValue);
+ }
+
+ foreach ($attributeValue as $value) {
+ if ($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute($value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a configured UserAccessMapper instance. The instance is configured
+ * using INI config option values.
+ *
+ * @return UserAccessMapper
+ */
+ public static function makeConfigured()
+ {
+ $result = new UserAccessMapper();
+ $result->setUserAccessAttributeParser(UserAccessAttributeParser::makeConfigured());
+
+ $viewAttributeName = Config::getLdapViewAccessField();
+ if (!empty($viewAttributeName)) {
+ $result->setViewAttributeName($viewAttributeName);
+ }
+
+ $adminAttributeName = Config::getLdapAdminAccessField();
+ if (!empty($adminAttributeName)) {
+ $result->setAdminAttributeName($adminAttributeName);
+ }
+
+ $superuserAttributeName = Config::getSuperUserAccessField();
+ if (!empty($superuserAttributeName)) {
+ $result->setSuperuserAttributeName($superuserAttributeName);
+ }
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/LdapInterop/UserMapper.php b/LdapInterop/UserMapper.php
new file mode 100644
index 0000000..f93ff28
--- /dev/null
+++ b/LdapInterop/UserMapper.php
@@ -0,0 +1,480 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\LdapInterop;
+
+use Exception;
+use Piwik\Access;
+use Piwik\API\Proxy;
+use Piwik\API\Request;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\Config;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Maps LDAP users to arrays that can be used to create new Piwik
+ * users.
+ *
+ * See {@link UserSynchronizer} for more information.
+ */
+class UserMapper
+{
+ const DEFAULT_USER_EMAIL_SUFFIX = '@mydomain.com';
+
+ const USER_PREFERENCE_NAME_IS_LDAP_USER = 'isLDAPUser';
+
+ /**
+ * The LDAP resource field that holds a user's username.
+ *
+ * @var string
+ */
+ private $ldapUserIdField = 'uid';
+
+ /**
+ * The LDAP resource field to use when determining a user's alias.
+ *
+ * @var string
+ */
+ private $ldapAliasField = 'cn';
+
+ /**
+ * The LDAP resource field to use when determining a user's email address.
+ *
+ * @var string
+ */
+ private $ldapMailField = 'mail';
+
+ /**
+ * The LDAP resource field to use when determining a user's first name.
+ *
+ * @var string
+ */
+ private $ldapFirstNameField = 'givenname';
+
+ /**
+ * The LDAP resource field to use when determining a user's last name.
+ *
+ * @var string
+ */
+ private $ldapLastNameField = 'sn';
+
+ /**
+ * The LDAP resource field to use when determining a user's password.
+ *
+ * @var string
+ */
+ private $ldapUserPasswordField = 'userpassword';
+
+ /**
+ * Suffix to be appended to user names of LDAP users that have no email address.
+ * Email addresses are required for Piwik users, so something must be entered.
+ *
+ * @var string
+ */
+ private $userEmailSuffix = self::DEFAULT_USER_EMAIL_SUFFIX;
+
+ /**
+ * If true, the user email suffix is appended to the Piwik user's login. This means
+ * the DB will store the user's login w/ the suffix, but user's will login without
+ * the suffix. This emulates pre-3.0 behavior and is necessary for backwards
+ * compatibility.
+ *
+ * @var bool
+ */
+ private $appendUserEmailSuffixToUsername = true;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Creates an array with normal Piwik user information using LDAP data for the user. The
+ * information in the result should be used with the **UsersManager.addUser** API method.
+ *
+ * This method is used in syncing LDAP user information with Piwik user info.
+ *
+ * @param string[] $ldapUser Associative array containing LDAP field data, eg, `array('dn' => '...')`
+ * @param string[]|null $piwikUser The existing Piwik user or null if none exists yet.
+ * @return string[]
+ */
+ public function createPiwikUserFromLdapUser($ldapUser, $user = null)
+ {
+ $login = $this->getRequiredLdapUserField($ldapUser, $this->ldapUserIdField);
+
+ return array(
+ 'login' => $login,
+ 'password' => $this->getPiwikPasswordForLdapUser($ldapUser, $user),
+ 'email' => $this->getEmailAddressForLdapUser($ldapUser, $login),
+ 'alias' => $this->getAliasForLdapUser($ldapUser)
+ );
+ }
+
+ /**
+ * Returns the expected LDAP username using a Piwik login. If a user email suffix is
+ * configured, it is appended to the login. This is to provide compatible behavior
+ * with old versions of the plugin.
+ *
+ * @param string $login The Piwik login.
+ * @return string The expected LDAP login.
+ */
+ public function getExpectedLdapUsername($login)
+ {
+ if (!empty($this->userEmailSuffix)
+ && $this->appendUserEmailSuffixToUsername
+ && $this->userEmailSuffix != self::DEFAULT_USER_EMAIL_SUFFIX
+ ) {
+ $login .= $this->userEmailSuffix;
+ }
+ return $login;
+ }
+
+ /**
+ * The password we store for a mapped user isn't used to authenticate, it's just
+ * data used to generate a user's token auth.
+ */
+ private function getPiwikPasswordForLdapUser($ldapUser, $user)
+ {
+ $ldapPassword = $this->getLdapUserField($ldapUser, $this->ldapUserPasswordField);
+
+ if (!empty($user['password'])) {
+ // do not generate new passwords for users that are already synchronized
+ return $user['password'];
+ } elseif (!empty($ldapPassword)) {
+ return $this->hashLdapPassword($ldapPassword);
+ } else {
+ $this->logger->warning("UserMapper::{func}: Could not find LDAP password for user '{user}', generating random one.",
+ array(
+ 'func' => __FUNCTION__,
+ 'user' => @$ldapUser[$this->ldapUserIdField]
+ ));
+
+ return $this->generateRandomPassword();
+ }
+ }
+
+ /**
+ * Generates a random string to be used as the 'dummy' password stored in the MySQL DB.
+ *
+ * @return string
+ */
+ public function generateRandomPassword()
+ {
+ return $this->hashLdapPassword(uniqid());
+ }
+
+ private function getEmailAddressForLdapUser($ldapUser, $login)
+ {
+ $email = $this->getLdapUserField($ldapUser, $this->ldapMailField);
+ if (empty($email)) { // a valid email is needed to create a new user
+ $email = $login;
+ if (strpos($email, '@') === false) {
+ $email .= $this->userEmailSuffix;
+ }
+ }
+ return $email;
+ }
+
+ private function getAliasForLdapUser($ldapUser)
+ {
+ $alias = $this->getLdapUserField($ldapUser, $this->ldapAliasField);
+ if (empty($alias)
+ && !empty($ldapUser[$this->ldapFirstNameField])
+ && !empty($ldapUser[$this->ldapLastNameField])
+ ) {
+ $alias = $this->getRequiredLdapUserField($ldapUser, $this->ldapFirstNameField)
+ . ' '
+ . $this->getRequiredLdapUserField($ldapUser, $this->ldapLastNameField);
+ }
+ return $alias;
+ }
+
+ private function getRequiredLdapUserField($ldapUser, $fieldName, $fetchSingleValue = true)
+ {
+ if (!isset($ldapUser[$fieldName])) {
+ throw new Exception("LDAP entity missing required '$fieldName' field.");
+ }
+
+ return $this->getLdapUserField($ldapUser, $fieldName, $fetchSingleValue);
+ }
+
+ private function getLdapUserField($ldapUser, $fieldName, $fetchSingleValue = true)
+ {
+ $result = @$ldapUser[$fieldName];
+ if ($fetchSingleValue
+ && is_array($result)
+ ) {
+ $result = reset($result);
+ }
+ return $result;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLdapUserIdField()
+ {
+ return $this->ldapUserIdField;
+ }
+
+ /**
+ * @param string $ldapUserIdField
+ */
+ public function setLdapUserIdField($ldapUserIdField)
+ {
+ $this->ldapUserIdField = strtolower($ldapUserIdField);
+ }
+
+ /**
+ * Returns the {@link $ldapAliasField} property.
+ *
+ * @return string
+ */
+ public function getLdapAliasField()
+ {
+ return $this->ldapAliasField;
+ }
+
+ /**
+ * Sets the {@link $ldapAliasField} property.
+ *
+ * @param string $ldapAliasField
+ */
+ public function setLdapAliasField($ldapAliasField)
+ {
+ $this->ldapAliasField = strtolower($ldapAliasField);
+ }
+
+ /**
+ * Returns the {@link $ldapMailField} property.
+ *
+ * @return string
+ */
+ public function getLdapMailField()
+ {
+ return $this->ldapMailField;
+ }
+
+ /**
+ * Sets the {@link $ldapMailField} property.
+ *
+ * @param string $ldapMailField
+ */
+ public function setLdapMailField($ldapMailField)
+ {
+ $this->ldapMailField = strtolower($ldapMailField);
+ }
+
+ /**
+ * Returns the {@link $ldapFirstNameField} property.
+ *
+ * @return string
+ */
+ public function getLdapFirstNameField()
+ {
+ return $this->ldapFirstNameField;
+ }
+
+ /**
+ * Sets the {@link $ldapFirstNameField} property.
+ *
+ * @param string $ldapFirstNameField
+ */
+ public function setLdapFirstNameField($ldapFirstNameField)
+ {
+ $this->ldapFirstNameField = strtolower($ldapFirstNameField);
+ }
+
+ /**
+ * Returns the {@link $ldapLastNameField} property.
+ *
+ * @return string
+ */
+ public function getLdapLastNameField()
+ {
+ return $this->ldapLastNameField;
+ }
+
+ /**
+ * Sets the {@link $ldapLastNameField} property.
+ *
+ * @param string $ldapLastNameField
+ */
+ public function setLdapLastNameField($ldapLastNameField)
+ {
+ $this->ldapLastNameField = strtolower($ldapLastNameField);
+ }
+
+ /**
+ * Returns the {@link $ldapUserPasswordField} property.
+ *
+ * @return string
+ */
+ public function getLdapUserPasswordField()
+ {
+ return $this->ldapUserPasswordField;
+ }
+
+ /**
+ * Sets the {@link $ldapUserPasswordField} property.
+ *
+ * @param string $userPasswordField
+ */
+ public function setLdapUserPasswordField($userPasswordField)
+ {
+ $this->ldapUserPasswordField = strtolower($userPasswordField);
+ }
+
+ /**
+ * Returns the {@link $userEmailSuffix} property.
+ *
+ * @return string
+ */
+ public function getUserEmailSuffix()
+ {
+ return $this->userEmailSuffix;
+ }
+
+ /**
+ * Sets the {@link $userEmailSuffix} property.
+ *
+ * @param string $userEmailSuffix
+ */
+ public function setUserEmailSuffix($userEmailSuffix)
+ {
+ $this->userEmailSuffix = $userEmailSuffix;
+ }
+
+ /**
+ * Returns the {@link $appendUserEmailSuffixToUsername} property.
+ *
+ * @return bool
+ */
+ public function getAppendUserEmailSuffixToUsername()
+ {
+ return $this->appendUserEmailSuffixToUsername;
+ }
+
+ /**
+ * Sets the {@link $appendUserEmailSuffixToUsername} property.
+ *
+ * @param bool $appendUserEmailSuffixToUsername
+ */
+ public function setAppendUserEmailSuffixToUsername($appendUserEmailSuffixToUsername)
+ {
+ $this->appendUserEmailSuffixToUsername = $appendUserEmailSuffixToUsername;
+ }
+
+ /**
+ * Hashes the LDAP password so no part the real LDAP password (or the hash stored in
+ * LDAP) will be stored in Piwik's DB.
+ */
+ protected function hashLdapPassword($password)
+ {
+ return md5($password);
+ }
+
+ /**
+ * Returns true if the user information is for a Piwik user that was mapped from LDAP,
+ * false if otherwise.
+ *
+ * @param string $userLogin The user login
+ * @return bool
+ */
+ public function isUserLdapUser($userLogin)
+ {
+ return Access::doAsSuperUser(function () use ($userLogin) {
+ $class = Request::getClassNameAPI('UsersManager');
+ $parameters = array(
+ 'userLogin' => $userLogin,
+ 'preferenceName' => self::USER_PREFERENCE_NAME_IS_LDAP_USER
+
+ );
+ $preference = Proxy::getInstance()->call($class, 'getUserPreference', $parameters);
+ return !!$preference;
+ });
+ }
+
+ /**
+ * Marks a user a synchronized LDAP user
+ *
+ * @param string $userLogin The user login
+ */
+ public function markUserAsLdapUser($userLogin)
+ {
+ Access::doAsSuperUser(function () use ($userLogin) {
+ $class = Request::getClassNameAPI('UsersManager');
+ $parameters = array(
+ 'userLogin' => $userLogin,
+ 'preferenceName' => self::USER_PREFERENCE_NAME_IS_LDAP_USER,
+ 'preferenceValue' => 1
+
+ );
+ Proxy::getInstance()->call($class, 'setUserPreference', $parameters);
+ });
+ }
+
+
+
+ /**
+ * Creates a UserMapper instance configured using INI options.
+ *
+ * @return UserMapper
+ */
+ public static function makeConfigured()
+ {
+ $result = new UserMapper();
+
+ $uidField = Config::getLdapUserIdField();
+ if (!empty($uidField)) {
+ $result->setLdapUserIdField($uidField);
+ }
+
+ $lastNameField = Config::getLdapLastNameField();
+ if (!empty($lastNameField)) {
+ $result->setLdapLastNameField($lastNameField);
+ }
+
+ $firstNameField = Config::getLdapFirstNameField();
+ if (!empty($firstNameField)) {
+ $result->setLdapFirstNameField($firstNameField);
+ }
+
+ $aliasField = Config::getLdapAliasField();
+ if (!empty($aliasField)) {
+ $result->setLdapAliasField($aliasField);
+ }
+
+ $mailField = Config::getLdapMailField();
+ if (!empty($mailField)) {
+ $result->setLdapMailField($mailField);
+ }
+
+ $userPasswordField = Config::getLdapPasswordField();
+ if (!empty($userPasswordField)) {
+ $result->setLdapUserPasswordField($userPasswordField);
+ }
+
+ $userEmailSuffix = Config::getLdapUserEmailSuffix();
+ if (!empty($userEmailSuffix)) {
+ $result->setUserEmailSuffix($userEmailSuffix);
+ }
+
+ $appendUserEmailSuffixToUsername = Config::shouldAppendUserEmailSuffixToUsername();
+ $result->setAppendUserEmailSuffixToUsername($appendUserEmailSuffixToUsername);
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/LdapInterop/UserSynchronizer.php b/LdapInterop/UserSynchronizer.php
new file mode 100644
index 0000000..5601cdc
--- /dev/null
+++ b/LdapInterop/UserSynchronizer.php
@@ -0,0 +1,333 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\LdapInterop;
+
+use Piwik\Access;
+use Piwik\API\Proxy;
+use Piwik\API\Request;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\UsersManager\Model as UserModel;
+use Piwik\Site;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Synchronizes LDAP user information with the Piwik database.
+ *
+ * LDAP user information is synchronized with the Piwik database every time a user
+ * logs in.
+ *
+ * ### Synchronizing User Information
+ *
+ * In order to display and use LDAP information without having to connect to LDAP
+ * on every request, some LDAP information is synchronized with Piwik's database.
+ *
+ * This information includes:
+ *
+ * - first name
+ * - last name
+ * - alias
+ * - email address
+ *
+ * **Allowing token_auth authentication**
+ *
+ * To allow authenticating by token auth for LDAP users, a dummy password is generated
+ * and stored in Piwik's database. Token auth authentication is then done in the same
+ * way as w/o any special Login.
+ *
+ * The generated password is prefixed with `{LDAP}` so LDAP users can be differentiated
+ * from normal users.
+ *
+ * ### Synchronizing User Access
+ *
+ * User access can be specified in custom LDAP attributes. To learn more, read the
+ * {@link UserAccessMapper} and {@link UserAccessAttributeParser} docs.
+ */
+class UserSynchronizer
+{
+ /**
+ * UserMapper instance used to map LDAP users to Piwik user entities.
+ *
+ * @var UserMapper
+ */
+ private $userMapper;
+
+ /**
+ * UserAccessMapper instance used to determine Piwik user access using LDAP user entities.
+ *
+ * @var UserAccessMapper
+ */
+ private $userAccessMapper;
+
+ /**
+ * UsersManager API instance used to add and get users.
+ *
+ * @var \Piwik\Plugins\UsersManager\API
+ */
+ private $usersManagerApi;
+
+ /**
+ * UserModel instance used to access user data. We don't go through the API in
+ * order to avoid thrown exceptions.
+ *
+ * @var UserModel
+ */
+ private $userModel;
+
+ /**
+ * The site IDs to grant view access to for every new LDAP user that is synchronized.
+ * Defaults to the `[LoginLdap] new_user_default_sites_view_access` INI config option.
+ *
+ * @var int[]
+ */
+ private $newUserDefaultSitesWithViewAccess = array();
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Converts a supplied LDAP entity into a Piwik user that is persisted in
+ * the MySQL DB.
+ *
+ * @param string $piwikLogin The username of the user who will be synchronized.
+ * @param string[] $ldapUser The LDAP user, eg, `array('uid' => ..., 'objectclass' => array(...), ...)`.
+ * @return string[] The Piwik user that was added. Will not contain the MD5 password
+ * hash in order to prevent accidental leaks.
+ */
+ public function synchronizeLdapUser($piwikLogin, $ldapUser)
+ {
+ $userMapper = $this->userMapper;
+ $usersManagerApi = $this->usersManagerApi;
+ $userModel = $this->userModel;
+ $newUserDefaultSitesWithViewAccess = $this->newUserDefaultSitesWithViewAccess;
+ $logger = $this->logger;
+ return Access::doAsSuperUser(function () use ($piwikLogin, $ldapUser, $userMapper, $usersManagerApi, $userModel, $newUserDefaultSitesWithViewAccess, $logger) {
+ $piwikLogin = $userMapper->getExpectedLdapUsername($piwikLogin);
+
+ $existingUser = $userModel->getUser($piwikLogin);
+
+ $user = $userMapper->createPiwikUserFromLdapUser($ldapUser, $existingUser);
+
+ $logger->debug("UserSynchronizer::{func}: synchronizing user [ piwik login = {piwikLogin}, ldap login = {ldapLogin} ]", array(
+ 'func' => 'synchronizeLdapUser',
+ 'piwikLogin' => $piwikLogin,
+ 'ldapLogin' => $user['login']
+ ));
+
+ if (empty($existingUser)) {
+ $usersManagerApi->addUser($user['login'], $user['password'], $user['email'], $user['alias'], $isPasswordHashed = true);
+
+ // set new user view access
+ if (!empty($newUserDefaultSitesWithViewAccess)) {
+ $usersManagerApi->setUserAccess($user['login'], 'view', $newUserDefaultSitesWithViewAccess);
+ }
+ } else {
+ if (!$userMapper->isUserLdapUser($existingUser['login'])) {
+ $logger->warning("Unable to synchronize LDAP user '{user}', non-LDAP user with same name exists.", array('user' => $existingUser['login']));
+ } else {
+ $usersManagerApi->updateUser($user['login'], $user['password'], $user['email'], $user['alias'], $isPasswordHashed = true);
+ }
+ }
+
+ $userMapper->markUserAsLdapUser($user['login']);
+
+ return $userModel->getUser($user['login']);
+ });
+ }
+
+ /**
+ * Uses information in LDAP user entity to set access levels in Piwik.
+ *
+ * @param string $piwikLogin The username of the Piwik user whose access will be set.
+ * @param string[] $ldapUser The LDAP entity to use when synchronizing.
+ */
+ public function synchronizePiwikAccessFromLdap($piwikLogin, $ldapUser)
+ {
+ if (empty($this->userAccessMapper)) {
+ return;
+ }
+
+ $userAccess = $this->userAccessMapper->getPiwikUserAccessForLdapUser($ldapUser);
+ if (empty($userAccess)) {
+ $this->logger->warning("UserSynchronizer::{func}: User '{user}' has no access in LDAP, but access synchronization is enabled.", array(
+ 'func' => __FUNCTION__,
+ 'user' => $piwikLogin
+ ));
+
+ return;
+ }
+
+ // for the synchronization, need to reset all user accesses
+ $this->userModel->deleteUserAccess($piwikLogin);
+ $this->userModel->setSuperUserAccess($piwikLogin,false);
+
+ $usersManagerApi = $this->usersManagerApi;
+ foreach ($userAccess as $userAccessLevel => $sites) {
+ Access::doAsSuperUser(function () use ($usersManagerApi, $userAccessLevel, $sites, $piwikLogin) {
+ if ($userAccessLevel == 'superuser') {
+ $usersManagerApi->setSuperUserAccess($piwikLogin, true);
+ } else {
+ $usersManagerApi->setUserAccess($piwikLogin, $userAccessLevel, $sites);
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the {@link $userMapper} password.
+ *
+ * @return UserMapper
+ */
+ public function getUserMapper()
+ {
+ return $this->userMapper;
+ }
+
+ /**
+ * Sets the {@link $userMapper} password.
+ *
+ * @param UserMapper $userMapper
+ */
+ public function setUserMapper(UserMapper $userMapper)
+ {
+ $this->userMapper = $userMapper;
+ }
+
+ /**
+ * Gets the {@link $usersManagerApi} property.
+ *
+ * @return UsersManagerAPI
+ */
+ public function getUsersManagerApi()
+ {
+ return $this->usersManagerApi;
+ }
+
+ /**
+ * Sets the {@link $usersManagerApi} property.
+ *
+ * @param UsersManagerAPI $usersManagerApi
+ */
+ public function setUsersManagerApi(UsersManagerAPI $usersManagerApi)
+ {
+ $this->usersManagerApi = $usersManagerApi;
+ }
+
+ /**
+ * Gets the {@link $newUserDefaultSitesWithViewAccess} property.
+ *
+ * @return int[]
+ */
+ public function getNewUserDefaultSitesWithViewAccess()
+ {
+ return $this->newUserDefaultSitesWithViewAccess;
+ }
+
+ /**
+ * Sets the {@link $newUserDefaultSitesWithViewAccess} property.
+ *
+ * @param int[] $newUserDefaultSitesWithViewAccess
+ */
+ public function setNewUserDefaultSitesWithViewAccess(array $newUserDefaultSitesWithViewAccess)
+ {
+ $this->newUserDefaultSitesWithViewAccess = $newUserDefaultSitesWithViewAccess;
+ }
+
+ /**
+ * Gets the {@link $userModel} property.
+ *
+ * @return UserModel
+ */
+ public function getUserModel()
+ {
+ return $this->userModel;
+ }
+
+ /**
+ * Sets the {@link $userModel} property.
+ *
+ * @param UserModel $userModel
+ */
+ public function setUserModel($userModel)
+ {
+ $this->userModel = $userModel;
+ }
+
+ /**
+ * Gets the {@link $userAccessMapper} property.
+ *
+ * @return UserAccessMapper
+ */
+ public function getUserAccessMapper()
+ {
+ return $this->userAccessMapper;
+ }
+
+ /**
+ * Sets the {@link $userAccessMapper} property.
+ *
+ * @param UserAccessMapper $userAccessMapper
+ */
+ public function setUserAccessMapper($userAccessMapper)
+ {
+ $this->userAccessMapper = $userAccessMapper;
+ }
+
+ /**
+ * Creates a UserSynchronizer using INI configuration.
+ *
+ * @return UserSynchronizer
+ */
+ public static function makeConfigured()
+ {
+ $result = new UserSynchronizer();
+ $result->setUserMapper(UserMapper::makeConfigured());
+ $result->setUsersManagerApi(UsersManagerAPI::getInstance());
+ $result->setUserModel(new UserModel());
+
+ /** @var LoggerInterface $logger */
+ $logger = StaticContainer::get('Psr\Log\LoggerInterface');
+
+ if (Config::isAccessSynchronizationEnabled()) {
+ $result->setUserAccessMapper(UserAccessMapper::makeConfigured());
+
+ $logger->debug("UserSynchronizer::{func}(): Using UserAccessMapper when synchronizing users.", array('func' => __FUNCTION__));
+ } else {
+ $logger->debug("UserSynchronizer::{func}(): LDAP access synchronization not enabled.", array('func' => __FUNCTION__));
+ }
+
+ $defaultSitesWithViewAccess = Config::getDefaultSitesToGiveViewAccessTo();
+ if (!empty($defaultSitesWithViewAccess)) {
+ $siteIds = Access::doAsSuperUser(function () use ($defaultSitesWithViewAccess) {
+ return Site::getIdSitesFromIdSitesString($defaultSitesWithViewAccess);
+ });
+
+ if (empty($siteIds)) {
+ $logger->warning("UserSynchronizer::{func}(): new_user_default_sites_view_access INI config option has no "
+ . "entries. Newly synchronized users will not have any access.", array('func' => __FUNCTION__));
+ }
+
+ $result->setNewUserDefaultSitesWithViewAccess($siteIds);
+ }
+
+ $logger->debug("UserSynchronizer::{func}: configuring with defaultSitesWithViewAccess = {sites}", array(
+ 'func' => __FUNCTION__,
+ 'sites' => $defaultSitesWithViewAccess
+ ));
+
+ return $result;
+ }
+}
diff --git a/LoginLdap.php b/LoginLdap.php
new file mode 100644
index 0000000..761dca5
--- /dev/null
+++ b/LoginLdap.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Exception;
+use Piwik\Access;
+use Piwik\Auth;
+use Piwik\Common;
+use Piwik\Container\StaticContainer;
+use Piwik\FrontController;
+use Piwik\Menu\MenuAdmin;
+use Piwik\Piwik;
+use Piwik\Plugin\Manager;
+use Piwik\Plugins\Login\Login;
+use Piwik\Plugins\LoginLdap\Auth\Base as AuthBase;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Session;
+use Piwik\View;
+
+/**
+ *
+ * @package LoginLdap
+ */
+class LoginLdap extends \Piwik\Plugin
+{
+ /**
+ * @return array
+ */
+ public function registerEvents()
+ {
+ $hooks = array(
+ 'Request.initAuthenticationObject' => 'initAuthenticationObject',
+ 'User.isNotAuthorized' => 'noAccess',
+ 'API.Request.authenticate' => 'ApiRequestAuthenticate',
+ 'AssetManager.getJavaScriptFiles' => 'getJsFiles',
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'Controller.Login.resetPassword' => 'disablePasswordResetForLdapUsers',
+ 'Controller.LoginLdap.resetPassword' => 'disablePasswordResetForLdapUsers',
+ 'Controller.Login.confirmResetPassword' => 'disableConfirmResetPasswordForLdapUsers',
+ 'UsersManager.checkPassword' => 'checkPassword',
+ );
+ return $hooks;
+ }
+
+ public function getJsFiles(&$jsFiles)
+ {
+ $jsFiles[] = "plugins/Login/javascripts/login.js";
+ $jsFiles[] = "plugins/LoginLdap/angularjs/admin/admin.controller.js";
+ $jsFiles[] = "plugins/LoginLdap/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.js";
+ }
+
+ public function getStylesheetFiles(&$stylesheetFiles)
+ {
+ $stylesheetFiles[] = "plugins/Login/stylesheets/login.less";
+ $stylesheetFiles[] = "plugins/Login/stylesheets/variables.less";
+ $stylesheetFiles[] = "plugins/LoginLdap/angularjs/admin/admin.controller.less";
+ $stylesheetFiles[] = "plugins/LoginLdap/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.less";
+ }
+
+ public function getClientSideTranslationKeys(&$keys)
+ {
+ $keys[] = "General_NUsers";
+ $keys[] = "LoginLdap_OneUser";
+ $keys[] = "LoginLdap_MemberOfCount";
+ $keys[] = "LoginLdap_FilterCount";
+ $keys[] = "LoginLdap_Test";
+ $keys[] = "General_NUsers";
+ }
+
+ /**
+ * Deactivate default Login module, as both cannot be activated together
+ *
+ * TODO: shouldn't disable Login plugin but have to wait until Dependency Injection is added to core
+ */
+ public function activate()
+ {
+ if (Manager::getInstance()->isPluginActivated("Login") == true) {
+ Manager::getInstance()->deactivatePlugin("Login");
+ }
+ }
+
+ /**
+ * Activate default Login module, as one of them is needed to access Piwik
+ */
+ public function deactivate()
+ {
+ if (Manager::getInstance()->isPluginActivated("Login") == false) {
+ Manager::getInstance()->activatePlugin("Login");
+ }
+ }
+
+ public function disableConfirmResetPasswordForLdapUsers()
+ {
+ $login = Common::getRequestVar('login', false);
+ if (empty($login)) {
+ return;
+ }
+
+ if ($this->isUserLdapUser($login)) {
+ // redirect to login w/ error message
+ $errorMessage = Piwik::translate("LoginLdap_UnsupportedPasswordReset");
+ echo FrontController::getInstance()->dispatch('LoginLdap', 'login', array($errorMessage));
+
+ exit;
+ }
+ }
+
+ public function disablePasswordResetForLdapUsers()
+ {
+ $login = Common::getRequestVar('form_login', false);
+ if (empty($login)) {
+ return;
+ }
+
+ if ($this->isUserLdapUser($login)) {
+ $errorMessage = Piwik::translate("LoginLdap_UnsupportedPasswordReset");
+
+ $view = new View("@Login/resetPassword");
+ $view->infoMessage = null;
+ $view->formErrors = array($errorMessage);
+
+ echo $view->render();
+
+ exit;
+ }
+ }
+
+ /**
+ * Redirects to Login form with error message.
+ * Listens to User.isNotAuthorized hook.
+ */
+ public function noAccess(Exception $exception)
+ {
+ $exceptionMessage = $exception->getMessage();
+
+ echo FrontController::getInstance()->dispatch('LoginLdap', 'login', array($exceptionMessage));
+ }
+
+ /**
+ * Initializes the authentication object.
+ * Listens to Request.initAuthenticationObject hook.
+ */
+ function initAuthenticationObject($activateCookieAuth = false)
+ {
+ $auth = AuthBase::factory();
+ StaticContainer::getContainer()->set('Piwik\Auth', $auth);
+
+ Login::initAuthenticationFromCookie($auth, $activateCookieAuth);
+ }
+
+ /**
+ * Set login name and authentication token for authentication request.
+ * Listens to API.Request.authenticate hook.
+ */
+ public function ApiRequestAuthenticate($tokenAuth)
+ {
+ /** @var Auth $auth */
+ $auth = StaticContainer::get('Piwik\Auth');
+ $auth->setLogin($login = null);
+ $auth->setTokenAuth($tokenAuth);
+ }
+
+ private function isUserLdapUser($login)
+ {
+ $userMapper = new UserMapper();
+ return $userMapper->isUserLdapUser($login);
+ }
+
+ private function isCurrentUserLdapUser(Auth $auth)
+ {
+ $currentUserLogin = $auth->getLogin();
+
+ if (empty($currentUserLogin)) {
+ return false;
+ }
+
+ return $this->isUserLdapUser($auth->getLogin());
+ }
+
+ /**
+ * Throws Exception when LDAP user tries to change password
+ * because such user's pass should be managed directly on LDAP host
+ *
+ * @throws Exception
+ */
+ public function disablePasswordChangeForLdapUsers(Auth $auth)
+ {
+ if ($this->isCurrentUserLdapUser($auth)) {
+ throw new Exception(
+ Piwik::translate('LoginLdap_LdapUserCantChangePassword')
+ );
+ }
+ }
+
+ /**
+ * Listens to UsersManager.checkPassword hook.
+ */
+ public function checkPassword()
+ {
+ $auth = StaticContainer::get('Piwik\Auth');
+ $this->disablePasswordChangeForLdapUsers($auth);
+ }
+}
diff --git a/Menu.php b/Menu.php
new file mode 100644
index 0000000..83ea899
--- /dev/null
+++ b/Menu.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Piwik\Menu\MenuAdmin;
+use Piwik\Piwik;
+
+class Menu extends \Piwik\Plugin\Menu
+{
+ public function configureAdminMenu(MenuAdmin $menu)
+ {
+ if (Piwik::hasUserSuperUserAccess()) {
+ $menu->addSystemItem('LDAP', $this->urlForAction('admin'), $order = 30);
+ }
+ }
+} \ No newline at end of file
diff --git a/Model/LdapUsers.php b/Model/LdapUsers.php
new file mode 100644
index 0000000..572ca70
--- /dev/null
+++ b/Model/LdapUsers.php
@@ -0,0 +1,639 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\Model;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Db;
+use Piwik\Piwik;
+use Piwik\Plugins\LoginLdap\Config;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\LoginLdap\Ldap\Client as LdapClient;
+use Piwik\Plugins\LoginLdap\Ldap\ServerInfo;
+use Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException;
+use InvalidArgumentException;
+use Exception;
+use Psr\Log\LoggerInterface;
+
+/**
+ * DAO class for user related operations that use LDAP as a backend.
+ */
+class LdapUsers
+{
+ const FUNCTION_START_LOG_MESSAGE = "Model\\LdapUsers: start {function}() with {params}";
+ const FUNCTION_END_LOG_MESSAGE = "Model\\LdapUsers: end {function}() with {result}";
+
+ /**
+ * If set, the user must be a member of a specific LDAP groupOfNames in order
+ * to authenticate to Piwik. Users that are not a part of this group will not
+ * be able to access Piwik.
+ *
+ * @var string
+ */
+ private $authenticationRequiredMemberOf;
+
+ /**
+ *Field used by your LDAP to indicate membership, by default \"memberOf\"
+ * @var string
+ */
+ private $authenticationMemberOfField;
+
+ /**
+ * If set, this value is added to the end of usernames before authentication
+ * is attempted.
+ *
+ * @var string
+ */
+ private $authenticationUsernameSuffix;
+
+ /**
+ * An LDAP filter that should be used to further filter LDAP users. Users that
+ * do not pass this filter will not be able to access Piwik.
+ *
+ * @var string ie, `"(&!((uidNumber=1002))(gidNumber=550))"`
+ */
+ private $authenticationLdapFilter;
+
+ /**
+ * The fully qualified class name of the LDAP client to use. Mostly for testing purposes,
+ * but it might have some future use.
+ *
+ * @var string
+ */
+ private $ldapClientClass = "Piwik\\Plugins\\LoginLdap\\Ldap\\Client";
+
+ /**
+ * Information describing the list of LDAP servers that should be used.
+ * When connecting, we try to connect with the first available server.
+ *
+ * @var ServerInfo[]
+ */
+ private $ldapServers = null;
+
+ /**
+ * The current LDAP client object if any. It is set when the {@link $doWithClient}
+ * method creates a Client and unset when the same method is done with a client.
+ *
+ * @var LdapClient|null
+ */
+ private $ldapClient;
+
+ /**
+ * The current {@link ServerInfo} instance describing the LDAP server we are
+ * currently connected to. It is set to the ServerInfo instance in {@link $servers}
+ * that describes the connected server. It is used to get server specific
+ * information such as the server's base DN or the admin user to bind with for
+ * the server.
+ *
+ * @var ServerInfo
+ */
+ private $currentServerInfo;
+
+ /**
+ * The UserMapper instance used to get the LDAP user ID field to use. Used when
+ * searching for a specific user.
+ *
+ * @var UserMapper
+ */
+ private $ldapUserMapper;
+
+ /**
+ * The timeout value in seconds for all LDAP network requests.
+ *
+ * @var int
+ */
+ private $ldapNetworkTimeout = LdapClient::DEFAULT_TIMEOUT_SECS;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ /**
+ * Authenticates a username/password pair using LDAP and returns LDAP user info on success.
+ *
+ * @param string $username The LDAP user's username. This is the value of the LDAP field specified by
+ * {@link $ldapUserIdField}.
+ * @param string $password The password to try and authenticate.
+ * @param bool $alreadyAuthenticated Whether to assume the user has already been authenticated or not.
+ * If true, we make sure the user is allowed to access Piwik based on
+ * the {@link $authenticationRequiredMemberOf} and {@link $authenticationLdapFilter}
+ * fields.
+ * @return array|null On success, returns user info stored in the LDAP database. On failure returns `null`.
+ * @throws ConnectionException if we connect to any configured LDAP server.
+ */
+ public function authenticate($username, $password, $alreadyAuthenticated = false)
+ {
+ $this->logger->debug(self::FUNCTION_START_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'params' => array($username, "<password[length=" . strlen($password) . "]>", $alreadyAuthenticated)
+ ));
+
+ if (empty($username)) {
+ throw new InvalidArgumentException('No username supplied in Model\\LdapUsers::authenticate().');
+ }
+
+ // if password is empty, avoid connecting to the LDAP server
+ if (empty($password)
+ && !$alreadyAuthenticated
+ ) {
+ $this->logger->debug("LdapUsers::{function}: empty password, skipping authentication", array(
+ 'function' => __FUNCTION__
+ ));
+
+ return null;
+ }
+
+ try {
+ $authenticationRequiredMemberOf = $this->authenticationRequiredMemberOf;
+ $logger = $this->logger;
+ $result = $this->doWithClient(function (LdapUsers $self, LdapClient $ldapClient)
+ use ($username, $password, $alreadyAuthenticated, $authenticationRequiredMemberOf, $logger) {
+
+ $user = $self->getUser($username);
+
+ if (empty($user)) {
+ $logger->debug("LdapUsers::{function}: No such user '{user}' or user is not a member of '{group}'.", array(
+ 'function' => __FUNCTION__,
+ 'user' => $username,
+ 'group' => $authenticationRequiredMemberOf
+ ));
+
+ return null;
+ }
+
+ if ($alreadyAuthenticated) {
+ $logger->debug("LdapUsers::{function}: assuming user {user} already authenticated, skipping LDAP authentication", array(
+ 'function' => __FUNCTION__,
+ 'user' => $username
+ ));
+
+ return $user;
+ }
+
+ if (empty($user['dn'])) {
+ $logger->debug("LdapUsers::{function}: LDAP user info for '{user}' has no dn attribute! (info = {info})", array(
+ 'function' => __FUNCTION__,
+ 'user' => $username,
+ 'info' => array_keys($user)
+ ));
+
+ return null;
+ }
+
+ if ($ldapClient->bind($user['dn'], $password)) {
+ return $user;
+ } else {
+ return null;
+ }
+ });
+ } catch (ConnectionException $ex) {
+ throw $ex;
+ } catch (Exception $ex) {
+ $this->logger->debug("LDAP authentication failure: {message}", array('message' => $ex->getMessage(), 'exception' => $ex));
+
+ $result = null;
+ }
+
+ $this->logger->debug(self::FUNCTION_END_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'result' => $result === null ? 'null' : array_keys($result)
+ ));
+
+ return $result;
+ }
+
+ /**
+ * Retrieves LDAP user information for a given username.
+ *
+ * @param string $username The username of the user to get LDAP information for.
+ * @return string[] Associative array containing LDAP field data, eg, `array('dn' => '...')`
+ */
+ public function getUser($username)
+ {
+ $this->logger->debug(self::FUNCTION_START_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'params' => array($username)
+ ));
+
+ $result = $this->doWithClient(function (LdapUsers $self, LdapClient $ldapClient, ServerInfo $server)
+ use ($username) {
+ $self->bindAsAdmin($ldapClient, $server);
+
+ // look for the user, applying extra filters
+ list($filter, $bind) = $self->getUserEntryQuery($username);
+ $userEntries = $ldapClient->fetchAll($server->getBaseDn(), $filter, $bind);
+
+ if ($userEntries === null) { // sanity check
+ throw new Exception("LDAP search for entries failed. (Unexpected Error, ldap_search returned null)");
+ }
+
+ if (empty($userEntries)) {
+ return null;
+ } else {
+ return $userEntries[0];
+ }
+ });
+
+ $this->logger->debug(self::FUNCTION_END_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'result' => $result === null ? 'null' : array_keys($result)
+ ));
+
+ return $result;
+ }
+
+ /**
+ * Returns count of users in LDAP that match an LDAP filter.
+ *
+ * @param string $filter The filter to match.
+ * @param string[] $filterBind Bind parameters for the filter.
+ * @return int
+ * @throws Exception if no LDAP server can be reached, if we cannot bind to the admin user, if
+ * the LDAP filter is incorrect, or if something else goes wrong during LDAP.
+ */
+ public function getCountOfUsersMatchingFilter($filter, $filterBind = array())
+ {
+ $this->logger->debug(self::FUNCTION_START_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'params' => $filter
+ ));
+
+ $result = $this->doWithClient(function (LdapUsers $self, LdapClient $ldapClient, ServerInfo $server)
+ use ($filter, $filterBind) {
+ $self->bindAsAdmin($ldapClient, $server);
+
+ return $ldapClient->count($server->getBaseDn(), $filter, $filterBind);
+ });
+
+ $this->logger->debug(self::FUNCTION_END_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'params' => $result
+ ));
+
+ return $result;
+ }
+
+ /**
+ * Returns all usernames found in LDAP after applying the configured filter and memberof
+ * requirement.
+ *
+ * @return string[]
+ * @throws Exception if no LDAP server can be reached, if we cannot bind to the admin user,
+ * or if something else goes wrong during LDAP.
+ */
+ public function getAllUserLogins()
+ {
+ $this->logger->debug(self::FUNCTION_START_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'params' => ''
+ ));
+
+ $userIdField = $this->ldapUserMapper->getLdapUserIdField();
+ list($filter, $bind) = $this->getUserEntryQuery($username = null);
+ $result = $this->doWithClient(function (LdapUsers $self, LdapClient $ldapClient, ServerInfo $server) use ($userIdField, $filter, $bind) {
+ $self->bindAsAdmin($ldapClient, $server);
+
+ $entries = $ldapClient->fetchAll($server->getBaseDn(), $filter, $bind, $attributes = array($userIdField));
+
+ $userIds = array();
+ foreach ($entries as $entry) {
+ if (empty($entry[$userIdField])) {
+ continue;
+ }
+
+ $userId = $entry[$userIdField];
+ if (is_array($userId)) {
+ $userId = reset($userId);
+ }
+
+ $userIds[] = $userId;
+ }
+ return $userIds;
+ });
+
+ $this->logger->debug(self::FUNCTION_END_LOG_MESSAGE, array(
+ 'function' => __FUNCTION__,
+ 'result' => $result
+ ));
+
+ return $result;
+ }
+
+ /**
+ * Sets the {@link $authenticationRequiredMemberOf} member.
+ *
+ * @param string $authenticationRequiredMemberOf
+ */
+ public function setAuthenticationRequiredMemberOf($authenticationRequiredMemberOf)
+ {
+ $this->authenticationRequiredMemberOf = $authenticationRequiredMemberOf;
+ }
+
+ /**
+ * Sets the {@link $authenticationMemberOfField} member.
+ *
+ * @param string $authenticationMemberOfField
+ */
+ public function setAuthenticationMemberOfField($authenticationMemberOfField)
+ {
+ $this->authenticationMemberOfField = $authenticationMemberOfField;
+ }
+
+ /**
+ * Sets the {@link $authenticationUsernameSuffix} member.
+ *
+ * @param string $authenticationUsernameSuffix
+ */
+ public function setAuthenticationUsernameSuffix($authenticationUsernameSuffix)
+ {
+ $this->authenticationUsernameSuffix = $authenticationUsernameSuffix;
+ }
+
+ /**
+ * Sets the {@link $authenticationLdapFilter} member.
+ *
+ * @param string $authenticationLdapFilter
+ */
+ public function setAuthenticationLdapFilter($authenticationLdapFilter)
+ {
+ $this->authenticationLdapFilter = $authenticationLdapFilter;
+ }
+
+ /**
+ * Sets the {@link $ldapClientClass} member.
+ *
+ * @param string $ldapClientClass
+ */
+ public function setLdapClientClass($ldapClientClass)
+ {
+ $this->ldapClientClass = $ldapClientClass;
+ }
+
+ /**
+ * Returns the {@link $ldapServers} member.
+ *
+ * @return ServerInfo[]
+ */
+ public function getLdapServers()
+ {
+ return $this->ldapServers;
+ }
+
+ /**
+ * Sets the {@link $ldapServers} member.
+ *
+ * @param ServerInfo[] $ldapServers
+ */
+ public function setLdapServers($ldapServers)
+ {
+ $this->ldapServers = $ldapServers;
+ }
+
+ /**
+ * Sets the {@link $ldapUserMapper} member.
+ *
+ * @param UserMapper $ldapUserMapper
+ */
+ public function setLdapUserMapper(UserMapper $ldapUserMapper)
+ {
+ $this->ldapUserMapper = $ldapUserMapper;
+ }
+
+ /**
+ * Gets the {@link $ldapNetworkTimeout} member.
+ *
+ * @return int
+ */
+ public function getLdapNetworkTimeout()
+ {
+ return $this->ldapNetworkTimeout;
+ }
+
+ /**
+ * Sets the {@link $ldapNetworkTimeout} member.
+ *
+ * @param int $ldapNetworkTimeout
+ */
+ public function setLdapNetworkTimeout($ldapNetworkTimeout)
+ {
+ $this->ldapNetworkTimeout = $ldapNetworkTimeout;
+ }
+
+ /**
+ * Public only for use in closure.
+ */
+ public function getUserEntryQuery($username = null)
+ {
+ $bind = array();
+ $conditions = array();
+
+ if (!empty($this->authenticationLdapFilter)) {
+ $conditions[] = $this->authenticationLdapFilter;
+ }
+
+ if (!empty($this->authenticationRequiredMemberOf)) {
+ $conditions[] = "(".$this->authenticationMemberOfField."=?)";
+ $bind[] = $this->authenticationRequiredMemberOf;
+ }
+
+ if (!empty($username)) {
+ $conditions[] = "(" . $this->ldapUserMapper->getLdapUserIdField() . "=?)";
+ $bind[] = $this->addUsernameSuffix($username);
+ }
+
+ $filter = "(&" . implode('', $conditions) . ")";
+
+ return array($filter, $bind);
+ }
+
+ /**
+ * Public only for use in closure.
+ */
+ public function addUsernameSuffix($username)
+ {
+ if (!empty($this->authenticationUsernameSuffix)) {
+ $this->logger->debug("Model\\LdapUsers::{function}: Adding suffix '{suffix}' to username '{username}'.", array(
+ 'function' => __FUNCTION__,
+ 'suffix' => $this->authenticationUsernameSuffix,
+ 'username' => $username
+ ));
+ }
+
+ return $username . $this->authenticationUsernameSuffix;
+ }
+
+ /**
+ * Executes a closure with a connected LDAP client. If a client has already been
+ * created, the stored client will be used.
+ *
+ * Using this method allows users of this class & methods of this class to combine
+ * multiple calls without creating multiple LDAP connections.
+ *
+ * If an LDAP client is created, it will be closed before the end of this method.
+ *
+ * @param callable|null $function Should accept 3 parameters: The LdapUsers instance,
+ * a connected LdapClient instance and a ServerInfo
+ * instance that describes the LDAP server we are
+ * connected to.
+ * @return mixed Returns the result of the callback.
+ * @throws Exception Forwards exceptions thrown by the callback and throws LDAP
+ * exceptions.
+ */
+ public function doWithClient($function = null)
+ {
+ $closeClient = false;
+
+ try {
+ if ($this->ldapClient === null) {
+ $this->makeLdapClient();
+
+ $closeClient = true;
+ }
+
+ $result = $function($this, $this->ldapClient, $this->currentServerInfo);
+ } catch (Exception $ex) {
+ if ($closeClient
+ && isset($this->ldapClient)
+ ) {
+ try {
+ $this->closeLdapClient();
+ } catch (Exception $ex) {
+ $this->logger->debug("Failed to close LDAP client: {message}", array(
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+ }
+
+ throw $ex;
+ }
+
+ if ($closeClient) {
+ $this->closeLdapClient();
+ }
+
+ return $result;
+ }
+
+ private function makeLdapClient()
+ {
+ if (empty($this->ldapServers)) { // sanity check
+ throw new Exception("No LDAP servers configured in LdapUsers instance.");
+ }
+
+ $ldapClientClass = $this->ldapClientClass;
+ $this->ldapClient = is_string($ldapClientClass) ? new $ldapClientClass() : $ldapClientClass;
+
+ foreach ($this->ldapServers as $server) {
+ try {
+ $this->ldapClient->connect($server->getServerHostname(), $server->getServerPort(), $this->getLdapNetworkTimeout());
+ $this->currentServerInfo = $server;
+
+ $this->logger->info("LdapUsers::{function}: Using LDAP server {host}:{port}", array(
+ 'function' => __FUNCTION__,
+ 'host' => $server->getServerHostname(),
+ 'port' => $server->getServerPort()
+ ));
+
+ return;
+ } catch (Exception $ex) {
+ $this->logger->info("Model\\LdapUsers::{function}: Could not connect to LDAP server {host}:{port}: {message}", array(
+ 'function' => __FUNCTION__,
+ 'host' => $server->getServerHostname(),
+ 'post' => $server->getServerPort(),
+ 'message' => $ex->getMessage(),
+ 'exception' => $ex
+ ));
+ }
+ }
+
+ $this->throwCouldNotConnectException();
+ }
+
+ private function closeLdapClient()
+ {
+ $this->ldapClient->close();
+ $this->ldapClient = null;
+ }
+
+ private function throwCouldNotConnectException()
+ {
+ if (count($this->ldapServers) > 1) {
+ $message = Piwik::translate('LoginLdap_CannotConnectToServers', count($this->ldapServers));
+ } else {
+ $message = Piwik::translate("LoginLdap_CannotConnectToServer");
+ }
+
+ throw new ConnectionException($message);
+ }
+
+ /**
+ * Public only for use in closure.
+ */
+ public function bindAsAdmin(LdapClient $ldapClient, ServerInfo $server)
+ {
+ $adminUserName = $server->getAdminUsername();
+
+ // bind using the admin user which has at least read access to LDAP users
+ if (!$ldapClient->bind($adminUserName, $server->getAdminPassword())) {
+ throw new Exception("Could not bind as LDAP admin.");
+ }
+ }
+
+ /**
+ * Creates a new {@link LdapUsers} instance using config.ini.php values.
+ *
+ * @return LdapUsers
+ */
+ public static function makeConfigured()
+ {
+ $result = new LdapUsers();
+
+ $result->setLdapServers(Config::getConfiguredLdapServers());
+
+ if (Config::shouldAppendUserEmailSuffixToUsername()) {
+ $usernameSuffix = Config::getLdapUserEmailSuffix();
+ if (!empty($usernameSuffix)) {
+ $result->setAuthenticationUsernameSuffix($usernameSuffix);
+ }
+ }
+
+ $requiredMemberOf = Config::getRequiredMemberOf();
+ if (!empty($requiredMemberOf)) {
+ $result->setAuthenticationRequiredMemberOf($requiredMemberOf);
+ }
+
+ $memberOfField = Config::getRequiredMemberOfField();
+ if (!empty($memberOfField)) {
+ $result->setAuthenticationMemberOfField($memberOfField);
+ }
+
+
+ $filter = Config::getLdapUserFilter();
+ if (!empty($filter)) {
+ $result->setAuthenticationLdapFilter($filter);
+ }
+
+ $timeoutSecs = Config::getLdapNetworkTimeout();
+ if (!empty($timeoutSecs)) {
+ $result->setLdapNetworkTimeout($timeoutSecs);
+ }
+
+ $result->setLdapUserMapper(UserMapper::makeConfigured());
+
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b88a99
--- /dev/null
+++ b/README.md
@@ -0,0 +1,245 @@
+# Piwik LoginLdap Plugin
+
+[![Build Status](https://travis-ci.org/piwik/plugin-LoginLdap.svg?branch=master)](https://travis-ci.org/piwik/plugin-LoginLdap)
+[![Coverage Status](https://img.shields.io/coveralls/piwik/plugin-LoginLdap.svg)](https://coveralls.io/r/piwik/plugin-LoginLdap?branch=master)
+
+## Description
+
+Allows users in LDAP to log in to Piwik Analytics. Supports web server authentication (eg, for Kerberos SSO).
+
+LoginLdap authenticates with an LDAP server and uses LDAP information to personalize Piwik.
+
+### Installation
+
+To start using LoginLdap, follow these steps:
+
+1. Login as a superuser
+2. On the _Manage > Marketplace_ admin page, install the LoginLdap plugin
+3. On the _Manage > Plugins_ admin page, enable the LoginLdap plugin
+4. Navigate to the _Settings > LDAP_ page
+5. Enter and save settings for your LDAP servers
+
+ _Note: You can test your servers by entering something into the 'Required User Group' and clicking the test link that appears.
+ An error message will display if LoginLdap cannot connect to the LDAP server._
+
+6. You can now login with LDAP cedentials.
+
+_**Note:** LDAP users are not synchronized with Piwik until they are first logged in. This means you cannot access a token auth for an LDAP user until the user is synchronized.
+If you use the default LoginLdap configuration, you can synchronize all of your LDAP users at once using the `./console loginldap:synchronize-users` command._
+
+### Troubleshooting
+
+To troubleshoot any connectivity issues, read our [troubleshooting guide](https://github.com/piwik/plugin-LoginLdap/wiki/Troubleshooting).
+
+### Upgrading from 2.2.7
+
+Version 3.0.0 is a major rewrite of the plugin, so if you are upgrading for 2.2.7 you will have to do some extra work when upgrading:
+
+- Navigate tothe _Settings > LDAP_ admin page. If the configuration options look broken, make sure to reload your browser cache. You can do this by reloading the page, or through your browser's settings.
+
+- The admin user for servers must now be a full DN. In the LDAP settings page, change the admin name to be the full DN (ie, cn=...,dc=...).
+
+- Uncheck the `Use LDAP for authentication` checkbox
+
+ Version 2.2.7 and below used an authentication strategy where user passwords were stored both in Piwik and in LDAP. In order to keep your current
+ users' token auths from changing, that same strategy has to be used.
+
+### Configurations
+
+LoginLdap supports three different LDAP authentication strategies:
+
+- using LDAP for authentication only
+- using LDAP for synchronization only
+- logging in with Kerberos SSO (or something similar)
+
+Each strategy has advantages and disadvantages. What you should use depends on your needs.
+
+### Using LDAP for authentication only
+
+This strategy is more secure than the one below, but it requires connecting to the LDAP server on each login attempt.
+
+With this strategy, every time a user logs in, LoginLdap will connect to LDAP to authenticate. On successful login, the user can
+be synchronised, but the user's password is never stored in Piwik's DB, just in the LDAP server. Additionally, the token auth is generated using
+a hash of a hash of the password, or is generated randomly.
+
+This means that if the Piwik DB is ever compromised, your LDAP users' passwords will still be safe.
+
+_Note: With this auth strategy, non-LDAP users are still allowed to login to Piwik. These users must be created through Piwik, not in LDAP._
+
+**Steps to enable**
+
+_Note: this is the default configuration._
+
+1. Check the `Use LDAP for authentication` option and uncheck the `Use Web Server Auth (e.g. Kerberos SSO)` option.
+
+### Using LDAP for synchronization only
+
+This strategy involves storing the user's passwords in the Piwik DB using Piwik's hashing. As a result, it is not as secure as the above
+method. If your Piwik DB is compromised, your LDAP users' passwords will be in greater danger of being cracked.
+
+But, this strategy opens up the possibility of not communicating with LDAP servers at all during authentication, which may provide a better user experience.
+
+_Note: With this auth strategy, non-LDAP users can login to Piwik._
+
+**Steps to enable**
+
+1. Uncheck the `Use LDAP for authentication` option and uncheck the `Use Web Server Auth (e.g. Kerberos SSO)` option.
+2. If you don't want to connect to LDAP while logging in, uncheck the `Synchronize Users After Successful Login` option.
+
+ a. If you uncheck this option, make sure your users are synchronized in some other way (eg, by using the `loginldap:synchronize-users` command).
+ Piwik still needs information about your LDAP users in order to let them authenticate.
+
+### Logging in with Kerberos SSO (or something similar)
+
+This strategy delegates authentication to the webserver. You setup a system where the webserver authenticates the user and
+sets the `$_SERVER['REMOTE_USER']` server variable, and LoginLdap will assume the user is already authenticated.
+
+This strategy will still connect to an LDAP server in order to synchronize user information, unless configured not to.
+
+_Note: With this auth strategy, any user that appears as a REMOTE_USER can login, even if they are not in LDAP._
+
+**Steps to enable**
+
+1. Check the `Use Web Server Auth (e.g. Kerberos SSO)` option.
+2. If you don't want to connect to LDAP while logging in, uncheck the `Synchronize Users After Successful Login` option.
+
+ a. If you uncheck this option, make sure your users are synchronized in some other way (eg, by using the `loginldap:synchronize-users` command).
+ Piwik still needs information about your LDAP users in order to let them authenticate.
+
+## Features
+
+### Authenticating with Kerberos
+
+If you want to use Kerberos, check the **Use Web Server Auth (e.g. Kerberos SSO)** checkbox in the LDAP settings admin page.
+
+Then, make sure your web server performs the necessary authentication and sets the `$_SERVER['REMOTE_USER']` server variable when a user is authenticated.
+
+When the `$_SERVER['REMOTE_USER']` variable is set, LoginLdap will assume the user has already been authenticated. When `$_SERVER['REMOTE_USER']` variable
+is not set and "Always Use LDAP for Authentication" option is checked, LDAP authentication is performed. When "Always Use LDAP for Authentication" is unchecked,
+normal authentication will take place.
+
+_Note: The plugin will still communicate with the LDAP server in order to synchronize user details, so if LDAP settings are incorrect, authentication will fail._
+
+### Specifying Fallback Servers
+
+LoginLdap v3.0.0 and greater supports specifying multiple LDAP servers to use. If connecting to one server fails, the other servers are used as fallbacks.
+
+You can enter fallback servers by adding new servers at the bottom of the _Settings > LDAP_ page.
+
+### Filtering Users in LDAP
+
+You can use the **Required User Group** and **LDAP Search Filter** settings to filter LDAP entries. Users whose entries do not match these filters
+will not be allowed to authenticate.
+
+Set **Required User Group** to the full DN of a group the user should be a member of. _Note: Internally, LoginLdap will issue a query using `(memberof=?)`
+to find users of a certain group. Your server may require additional configuration to support `memberof`._
+
+Set **LDAP Search Filter** to an LDAP filter string to use, for example: `(objectClass=person)` or
+`(&(resMemberOf=cn=mygroup,ou=commonOU,dc=www,dc=example,dc=org)(objectclass=person))`.
+
+You can test both of these settings from within the LDAP settings page.
+
+### LDAP User Synchronization
+
+LoginLdap will use information in LDAP to determine a user's alias and email address. On the _Settings > LDAP_ page, you can specify which LDAP attributes should be
+use to determine these fields.
+
+_Note: If the LDAP attribute for a user's alias is not found, the user's alias is defaulted to the first and last names of the user. On the settings page you can
+specify which LDAP attributes are used to determine a user's first & last name._
+
+**E-mail addresses**
+
+E-mail addresses are required for Piwik users. If your users in LDAP do not have e-mail addresses, you can set the **E-mail Address Suffix** setting to an e-mail
+address suffix, for example:
+
+`@myorganization.com`
+
+The suffix will be added to usernames to generate an e-mail address for your users.
+
+Users are synchronized every time they log in. You can use the `loginldap:synchronize-users` command to synchronize users manually.
+
+### Piwik Access Synchronization
+
+LoginLdap also supports synchronizing access levels using attributes found in LDAP. To use this feature, first, you will need to modify your LDAP server's
+schema and add three special attributes to user entries:
+
+- an attribute to specify the sites a user has view access to
+- an attribute to specify the sites a user has admin access to
+- and an attribute used to specify if a user is a superuser or not
+
+_Note: You can choose whatever names you want for these attributes. You will be able to tell LoginLdap about these names in the LDAP settings page._
+
+Then you must set these attributes correctly within LDAP, for example:
+
+- `view: all`
+- `admin: 1,2,3`
+- `superuser: 1`
+
+Finally, in the LDAP settings page, check the **Enable User Access Synchronization from LDAP** checkbox and fill out the settings that appear below it.
+
+User access synchronization occurs at the same time as normal user synchronization. So the `loginldap:synchronize-users` command will synchronize access levels too.
+
+#### Managing Access for Multiple Piwik Instances
+
+LoginLdap supports using a single LDAP server to manage access for multiple Piwik instances. If you'd like to use this feature, you must specify special values
+for LDAP access attributes. For example:
+
+- `view: mypiwikserver.whatever.com:1,2,3;myotherserver.com:all`
+- `admin: mypiwikserver.whatever.com:all;mythirdserver.com:3,4`
+- `superuser: myotherserver.com;myotherserver.com/otherpiwik`
+
+If you don't want to use URLs in your access attributes, you can use the **Special Name For This Piwik Instance** setting to specify a special name
+for each of your Piwiks. For example, if you set it to 'piwikServerA' in one Piwik and 'piwikServerB' in another, your LDAP attributes might look
+like:
+
+- `view: piwikServerA:1,2,3;piwikServerB:all`
+- `admin: piwikServerA:4,5,6`
+- `superuser: piwikServerC`
+
+**Using a custom access attribute format**
+
+You can customize the separators used in access attributes by setting the **User Access Attribute Server Specification Delimiter** and
+**User Access Attribute Server & Site List Separator** settings.
+
+If you set the **User Access Attribute Server Specification Delimiter** option to `'#'`, access attributes can be specified as:
+
+`view: piwikServerA:1,2,3#piwikServerB:all`
+
+If you set the **User Access Attribute Server & Site List Separator** option to `'#'`, access attributes can be specified as:
+
+`view: piwikServerA#1,2,3;piwikServerB#all`
+
+## Security Considerations
+
+**User passwords**
+
+For added security, LoginLdap's default configuration will not store user passwords or a hash of a user password within Piwik's DB. So if the Piwik DB is compromised
+for whatever reason, user passwords will not be compromised.
+
+**Token Auths**
+
+LDAP has no concept of authentication tokens, so user token_auths are stored exclusively in Piwik's MySQL DB. If a token auth is compromised,
+you can have Piwik generate a new.
+
+**Logging**
+
+LoginLdap uses debug logging extensively so problems can be diagnosed quickly. The logs should not contain sensitive information, but _you
+should still disable DEBUG logging in production_.
+
+If you need to debug a problem, enable it temporarily by changing the `[log] log_level` and `[log] log_writers` core INI config options.
+If you use file logs, make sure to delete the logs after you are finished debugging.
+
+## Commands
+
+LoginLdap comes with the following console commands:
+
+* `loginldap:synchronize-users`: Can be used to synchronize one, multiple, or all users in LDAP at once. If you'd like to setup user access
+ within Piwik before a user logs in, this command should be used.
+
+## Changelog
+
+See [https://github.com/piwik/plugin-LoginLdap/blob/master/CHANGELOG.md](https://github.com/piwik/plugin-LoginLdap/blob/master/CHANGELOG.md).
+
+## Support
+
+**Please direct any feedback to [https://github.com/piwik/plugin-LoginLdap](https://github.com/piwik/plugin-LoginLdap).**
diff --git a/Updates/3.0.0.php b/Updates/3.0.0.php
new file mode 100644
index 0000000..fd9e12a
--- /dev/null
+++ b/Updates/3.0.0.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap;
+
+use Piwik\Option;
+use Piwik\Updater;
+use Piwik\Updates;
+
+/**
+ */
+class Updates_3_0_0 extends Updates
+{
+ public function doUpdate(Updater $updater)
+ {
+ // when updating from pre-3.0 versions, set use_ldap_for_authentication to 0 and make sure
+ // a warning displays in the UI to not set it to 1
+ \Piwik\Config::getInstance()->LoginLdap['use_ldap_for_authentication'] = 0;
+ \Piwik\Config::getInstance()->forceSave();
+
+ Option::set('LoginLdap_updatedFromPre3_0', 1);
+ }
+}
diff --git a/angularjs/admin/admin.controller.js b/angularjs/admin/admin.controller.js
new file mode 100644
index 0000000..77061a1
--- /dev/null
+++ b/angularjs/admin/admin.controller.js
@@ -0,0 +1,82 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+angular.module('piwikApp').controller('LoginLdapAdminController', function ($scope, $attrs, piwikApi) {
+ // LDAP server info management
+ $scope.servers = JSON.parse($attrs['servers']) || [];
+
+ $scope.servers.addServer = function () {
+ this.push({
+ name: "server" + (this.length + 1),
+ hostname: "",
+ port: 389,
+ base_dn: "",
+ admin_user: "",
+ admin_pass: ""
+ });
+ };
+
+ $scope.getSampleViewAttribute = function (config) {
+ return getSampleAccessAttribute(config, config.ldap_view_access_field, '1,2', '3,4');
+ };
+
+ $scope.getSampleAdminAttribute = function (config) {
+ return getSampleAccessAttribute(config, config.ldap_admin_access_field, 'all', 'all');
+ };
+
+ $scope.getSampleSuperuserAttribute = function (config) {
+ return getSampleAccessAttribute(config, config.ldap_superuser_access_field);
+ };
+
+ $scope.synchronizeUser = function (userLogin) {
+ $scope.synchronizeUserError = $scope.synchronizeUserDone = null;
+
+ $scope.currentSynchronizeUserRequest = piwikApi.post(
+ {
+ method: "LoginLdap.synchronizeUser"
+ },
+ {
+ login: userLogin
+ },
+ {
+ createErrorNotification: false
+ }
+ ).then(function (response) {
+ $scope.synchronizeUserDone = true;
+ }).catch(function (message) {
+ $scope.synchronizeUserError = message;
+ })['finally'](function () {
+ $scope.currentSynchronizeUserRequest = null;
+ });
+ };
+
+ function getSampleAccessAttribute(config, accessField, firstValue, secondValue) {
+ var result = accessField + ': ';
+
+ if (config.instance_name) {
+ result += config.instance_name;
+ } else {
+ result += window.location.hostname;
+ }
+ if (firstValue) {
+ result += config.user_access_attribute_server_separator + firstValue;
+ }
+
+ result += config.user_access_attribute_server_specification_delimiter;
+
+ if (config.instance_name) {
+ result += 'piwikB';
+ } else {
+ result += 'anotherhost.com';
+ }
+ if (secondValue) {
+ result += config.user_access_attribute_server_separator + secondValue;
+ }
+
+ return result;
+ }
+}); \ No newline at end of file
diff --git a/angularjs/admin/admin.controller.less b/angularjs/admin/admin.controller.less
new file mode 100644
index 0000000..1052c99
--- /dev/null
+++ b/angularjs/admin/admin.controller.less
@@ -0,0 +1,90 @@
+#ldapSettingsTable,#accessSyncSettings,#ldapUserMappingSettingsTable {
+ &,>table {
+ display: inline-table;
+ }
+
+ td {
+ vertical-align: top;
+ }
+
+ tr > td:first-child {
+ width:50%;
+ > *:first-child {
+ margin-top:8px;
+ display:inline-block;
+ min-height: initial;
+ }
+ }
+
+ tr > td:last-child {
+ width:179px;
+ }
+
+ input:not(.submit) {
+ margin-top:0;
+ width:179px;
+ padding-right:36px;
+ }
+
+ .form-description {
+ font-size: .9em;
+ margin-top: .6em;
+ margin-left: 2em;
+ display: inline-block;
+ padding-right:1em;
+ }
+
+ border-spacing: 0 1.5em;
+
+ .test-config-option-link {
+ font-size:.8em;
+ position:relative;
+ display:inline-block;
+ font-style:italic;
+ right:36px;
+ margin-right:-12px;
+ }
+}
+
+#ldapSettingsTable,#accessSyncSettings,#ldapUserMappingSettingsTable,#ldapServersTable {
+ .notification {
+ font-size: 12px;
+ padding: 8px;
+ margin-bottom:0;
+ }
+}
+
+#ldapServersTable {
+ td {
+ vertical-align:middle;
+ }
+
+ table {
+ border-spacing: 1.5em 0;
+ }
+
+ .form-description {
+ margin-left: 1em;
+ display:inline-block;
+ }
+}
+
+#expectedAccessAttributeFormat,#synchronizeIndividualUserForm,#pre300AlwaysUseLdapWarning {
+ display:inline-block;
+ vertical-align:top;
+ width: 300px;
+ left:100%;
+ margin-left:1.5em;
+ .notification,span,p {
+ font-size:13px;
+ }
+ input {
+ font-size:12px;
+ }
+}
+
+#synchronizeIndividualUserForm,#expectedAccessAttributeFormat {
+ .notification::before {
+ content:"";
+ }
+} \ No newline at end of file
diff --git a/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.html b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.html
new file mode 100644
index 0000000..6159491
--- /dev/null
+++ b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.html
@@ -0,0 +1,26 @@
+<div>
+ <div piwik-field title="{{ testableField.title }}"
+ inline-help="{{ testableField.inlineHelp }}"
+ name="{{ testableField.inputName }}"
+ uicontrol="text"
+ value="{{ testableField.inputValue }}"
+ piwik-onenter="testableField.testValue()"
+ ng-model="testableField.inputValue"
+ ng-change="testableField.testResult = testableField.testError = null" ></div>
+
+ <div piwik-save-button
+ saving="testableField.currentRequest !== null"
+ onconfirm="testableField.testValue()"
+ ng-show="testableField.inputValue"
+ value="{{ 'LoginLdap_Test'|translate }}"></div>
+
+ <div class="test-config-option-success" ng-show="testableField.testResult !== null">
+ <strong>
+ <span ng-if="testableField.testResult==1">{{ 'LoginLdap_OneUser'|translate }}</span>
+ <span ng-if="testableField.testResult!=1">{{ 'General_NUsers'|translate:testableField.testResult }}</span>
+ </strong>
+ </div>
+ <div class="test-config-option-error" ng-show="testableField.testError">
+ {{ testableField.testError }}
+ </div>
+</div> \ No newline at end of file
diff --git a/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.js b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.js
new file mode 100644
index 0000000..703e7b7
--- /dev/null
+++ b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.js
@@ -0,0 +1,103 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Input field w/ a test link that calls an AJAX method when click. The result (or error message)
+ * is displayed in a notificationnext to the input.
+ *
+ * <div piwik-piwik-login-ldap-testable-field>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikLoginLdapTestableField', piwikLoginLdapTestableField);
+
+ piwikLoginLdapTestableField.$inject = ['piwik', 'piwikApi', "$compile"];
+
+ function piwikLoginLdapTestableField(piwik, piwikApi, $compile) {
+ return {
+ restrict: 'A',
+ scope: {
+ value: '@',
+ name: '@',
+ successTranslation: '@',
+ testApiMethod: '=',
+ testApiMethodArg: '=',
+ inlineHelp: '@',
+ ngModel: '@',
+ title: '@'
+ },
+ templateUrl: 'plugins/LoginLdap/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.html?cb=' + piwik.cacheBuster,
+ controller: function($scope, $element)
+ {
+ var testableField = {};
+ testableField.inputValue = $scope.value;
+ testableField.testApiMethod = $scope.testApiMethod;
+ testableField.testApiMethodArg = $scope.testApiMethodArg;
+ testableField.inputName = $scope.name;
+ testableField.inlineHelp = $scope.inlineHelp;
+ testableField.title = $scope.title;
+
+ testableField.testResult = null;
+ testableField.testError = null;
+ testableField.testValue = null;
+ testableField.currentRequest = null;
+
+ $element.find('.test-config-option-success').attr('piwik-translate', $scope.successTranslation);
+ $compile($element.find('.test-config-option-success'));
+
+ function testValue() {
+ if (testableField.currentRequest) {
+ testableField.currentRequest.abort();
+ }
+
+ testableField.testError = null;
+ testableField.testResult = null;
+
+ if (!testableField.inputValue) {
+ return;
+ }
+
+ var requestOptions = {createErrorNotification: false},
+ getParams = {method: $scope.testApiMethod};
+ getParams[$scope.testApiMethodArg] = testableField.inputValue;
+
+ testableField.currentRequest = piwikApi.fetch(
+ getParams,
+ requestOptions
+ ).then(function (response) {
+ testableField.testResult = response.value === null ? null : parseInt(response.value);
+ }).catch(function (message) {
+ testableField.testError = message;
+ testableField.testResult = null;
+ })['finally'](function () {
+ testableField.currentRequest = null;
+ });
+ }
+
+ function setDescendantProp (obj, desc, value) {
+ var arr = desc.split('.');
+ if (arr.length > 0 && arr[0] != '') {
+ var prop = arr.shift();
+ obj[prop] = setDescendantProp(obj[prop], arr.length ? arr.join('.') : '', value);
+ } else {
+ obj = value;
+ }
+ return obj;
+ }
+
+ testableField.testValue = testValue;
+ $scope.testableField = testableField;
+
+ // set changed values to ngModel
+ $scope.$watch("testableField.inputValue",
+ function(value){
+ setDescendantProp($scope.$parent, $scope.ngModel, value);
+ }
+ );
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.less b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.less
new file mode 100644
index 0000000..9ec48f7
--- /dev/null
+++ b/angularjs/login-ldap-testable-field/login-ldap-testable-field.directive.less
@@ -0,0 +1,10 @@
+.test-config-option-error, .test-config-option-success {
+ display: inline-block;
+ margin-left: 15px;
+ font-weight: bold;
+ color: #D4291F;
+}
+
+.test-config-option-success {
+ color: #009874;
+} \ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..247d4b6
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,29 @@
+{
+ "name": "piwik/LoginLdap",
+ "version": "3.3.0",
+ "description": "LDAP authentication and synchronization for Piwik.",
+ "keywords": ["ldap", "login", "authentication", "active", "directory", "kerberos", "sso"],
+ "license": "GPL v3+",
+ "homepage": "https://github.com/piwik/plugin-LoginLdap",
+ "require": {
+ "piwik/piwik": ">=2.16.0",
+ "php": ">=5.4"
+ },
+ "authors": [
+ {
+ "name": "Piwik",
+ "email": "hello@piwik.org",
+ "homepage": "https://github.com/piwik"
+ },
+ {
+ "name": "Aivo Koger",
+ "email": "aivo.koger@gmail.com",
+ "homepage": "https://github.com/tehnotronic"
+ },
+ {
+ "name": "Stefan Kreuter",
+ "email": "info@gigatec.de",
+ "homepage": "http://www.gigatec.de"
+ }
+ ]
+}
diff --git a/lang/ar.json b/lang/ar.json
new file mode 100644
index 0000000..5a1a07a
--- /dev/null
+++ b/lang/ar.json
@@ -0,0 +1,21 @@
+{
+ "LoginLdap": {
+ "MenuLdap": "مستخدمو LDAP",
+ "LoadUser": "تحميل المستخدم من LDAP",
+ "LoadUserDescription": "يمكنك استخدام هذا النموذج لمزامنة مستخدم واحد في LDAP من واجهة الاستخدام.",
+ "LoadUserCommandDesc": "استخدم الأمر %s لمزامنة مستخدمين أكثر.",
+ "Settings": "إعدادات LDAP",
+ "ServerUrl": "عنوان URL للخادم",
+ "UserIdField": "حقل معرِّف المستخدم",
+ "UserNotFound": "لم أجد المستخدم \"%s\" !",
+ "NoUserName": "لم تدخل اسم المستخدم!",
+ "AdminPass": "كلمة مرور LDAP",
+ "MemberOf": "مجموعة المستخدمين المطلوبة",
+ "Filter": "مرشح بحث LDAP",
+ "LdapUserAdded": "تمت إضافة مستخدم LDAP !",
+ "Test": "اختبار",
+ "NetworkTimeout": "مهلة طلب شبكة LDAP (بالثواني)",
+ "Go": "إنطلق",
+ "MemberOfField": "حقل memberOf في LDAP"
+ }
+} \ No newline at end of file
diff --git a/lang/be.json b/lang/be.json
new file mode 100644
index 0000000..0613943
--- /dev/null
+++ b/lang/be.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Тэст"
+ }
+} \ No newline at end of file
diff --git a/lang/bg.json b/lang/bg.json
new file mode 100644
index 0000000..3b45869
--- /dev/null
+++ b/lang/bg.json
@@ -0,0 +1,6 @@
+{
+ "LoginLdap": {
+ "Test": "Тест",
+ "Go": "Пускане"
+ }
+} \ No newline at end of file
diff --git a/lang/bn.json b/lang/bn.json
new file mode 100644
index 0000000..221e486
--- /dev/null
+++ b/lang/bn.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Go": "যাও"
+ }
+} \ No newline at end of file
diff --git a/lang/ca.json b/lang/ca.json
new file mode 100644
index 0000000..c4dc316
--- /dev/null
+++ b/lang/ca.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Prova"
+ }
+} \ No newline at end of file
diff --git a/lang/cs.json b/lang/cs.json
new file mode 100644
index 0000000..57fbfdb
--- /dev/null
+++ b/lang/cs.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Přihlašovací zásuvný modul s použitím LDAP autentizace. Poskytuje alternativu ke standardnímu přihlašovacímu zásuvnému modulu.",
+ "MenuLdap": "LDAP uživatelé",
+ "LoadUser": "Načíst uživatele z LDAP",
+ "LoadUserDescription": "Pro synchronizaci jednoho LDAP uživatele můžete použít tento formulář.",
+ "LoadUserCommandDesc": "Pokud chcete synchronizovat více uživatelů, použijte příkaz %s.",
+ "Settings": "Nastavení LDAP",
+ "AliasField": "Pole aliasu",
+ "BaseDn": "Základní DN",
+ "LdapPort": "Port serveru",
+ "MailField": "Pole emailové adresy",
+ "ServerUrl": "URL serveru",
+ "UserIdField": "Pole id uživatele",
+ "AliasFieldDescription": "např. \"cn\"",
+ "MailFieldDescription": "( např. \"mail\" )",
+ "UserIdFieldDescription": "( např. \"uid\", AD: \"userPrincipalName\" )",
+ "UserNotFound": "Uživatel %s nenalezen!",
+ "NoUserName": "Nebylo zadáno uživatelské jméno!",
+ "UsernameSuffix": "Přípona\/doména uživatelského jména",
+ "UsernameSuffixDescription": "( např. \"@localhost.com\" )",
+ "AdminUser": "Uživatelské jméno LDAP",
+ "AdminUserDescription": "např. \"john\" - k dotazování na další uživatele musí mít právo číst",
+ "AdminPass": "Heslo LDAP",
+ "MemberOf": "Skupina Active Directory",
+ "MemberOfDescription": "( např. \"CN=Matomo User,CN=Users,DC=organization,DC=com\"",
+ "MemberOfDescription2": "Poznámka: Toto využívá LDAP filtr memberOf=?. To může vyžadovat konfiguraci LDAP serveru.",
+ "Filter": "LDAP vyhledávací filtr",
+ "FilterDescription": "( např. \"(objectClass=person)\" )",
+ "Kerberos": "Kerberos povolen",
+ "KerberosDescription": "Pokud zapnuto, zásuvný modul zkontroluje proměnnou $_SERVER['REMOTE_USER'] a předpokládá že je uživatel přihlášen. Pokud $_SERVER['REMOTE_USER'] není dostupný, všichni uživatelé jsou ověřeni v závislosti na dalších nastaveních.",
+ "LdapUserAdded": "LDAP uživatel byl úspěšně přidán!",
+ "LdapFunctionsMissing": "Chybí PHP funkce ldap_connect(), povolte ji ve vaší konfiguraci.",
+ "CannotConnectToServer": "Nepodařilo se připojit k LDAP serveru.",
+ "CannotConnectToServers": "Nepodařilo se připojit k žádnému z %s LDAP serverů.",
+ "LDAPServers": "Servery LDAP",
+ "OneUser": "1 uživatel",
+ "MemberOfCount": "%s jsou členy této skupiny.",
+ "FilterCount": "%s odpovídá tomuto filtru.",
+ "Test": "Vyzkoušet",
+ "InvalidFilter": "Neplatný filtr.",
+ "ServerName": "Jméno serveru",
+ "FirstNameField": "Pole křestního jména",
+ "FirstNameFieldDescription": "LDAP atribut obsahující křestní jméno uživatele.",
+ "LastNameField": "Pole příjmení",
+ "LastNameFieldDescription": "LDAP atribut obsahující příjmení uživatele.",
+ "FirstLastNameForAlias": "Křestní jméno a příjmení je použito k vytvoření aliasu, pokud ho nelze najít v LDAP adresáři.",
+ "NewUserDefaultSitesViewAccess": "Stránky s přístupem k zobrazení pro nové uživatele",
+ "NewUserDefaultSitesViewAccessDescription": "Pokud je určen, potom bude nově synchronizovaný LDAP uživatel mít přístup k zobrazení k těmto stránkám. Hodnota je seznam ID stránek, nebo 'all'.",
+ "EnableLdapAccessSynchronization": "Povolit synchronizaci uživatelů z LDAP serveru",
+ "EnableLdapAccessSynchronizationDescription": "Pokud je povoleno, úroveň uživatele je určena z vlastních atributů LDAP serveru. Upozorňujeme, že bude nutná změna schématu vašeho LDAP serveru.",
+ "LdapViewAccessField": "LDAP atribut přístupu pro zobrazení",
+ "LdapViewAccessFieldDescription": "Vlastní LDAP atribut, který určuje, k jakým stránkám má uživatel přístup k prohlížení.",
+ "LdapAdminAccessField": "LDAP pole administrátorského přístupu",
+ "LdapAdminAccessFieldDescription": "Vlastní LDAP atribut, který udává, ke kterým stránkám má uživatel administrátorský přístup.",
+ "LdapSuperUserAccessField": "LDAP pole superuživatelského přístupu",
+ "LdapSuperUserAccessFieldDescription": "Vlastní LDAP atribut, který udává, jestli je uživatel super-uživatel.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Oddělovač specifikací serverů v atributu uživatelského přístupu",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Řetězec, který odděluje specifikace serverů v atributu uživatelského přístupu. Pokud je nastaven na ';', pak bude atribut vypadat jako '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Oddělovač stránek & serverů v atributu uživatelského přístupu",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Řetězec, který odděluje ID instance Matomo serveru od seznamů ID stránek. Pokud je nastaven na ':', pak bude přístupový atribut vypadat jako '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Speciální jméno této instance Matomou",
+ "ThisMatomoInstanceNameDescription": "Speciální jméno identifikující tuto instanci v atributu přístupu. Pokud není zadáno, očekáváme základní URL tohoto Matomou.",
+ "UnsupportedPasswordReset": "Heslo tohoto uživatele nelze resetovat, kontaktujte prosím administrátora.",
+ "UserSyncSettings": "Nastavení synchronizace uživatelů",
+ "AccessSyncSettings": "Přistupovat k nastavením synchronizace",
+ "SynchronizeUsersAfterLogin": "Synchronizovat uživatele po úspěšném přihlášení",
+ "SynchronizeUsersAfterLoginDescription": "Pokud je povoleno, údaje uživatele v LDAP serveru budou po každém úspěšném přihlášení synchronizovány.",
+ "SynchronizeUsersAfterLoginDescription2": "Pokud je zakázáno, zásuvný modul nebude automaticky synchronizovat údaje uživatelů po autentizaci, pokud není povoleno přihlášení pomocí LDAP. Pokud to bude možné, spojením s LDAP serverem se bude vyhýbat.",
+ "UseLdapForAuthentication": "Vždy použít LDAP k ověření",
+ "UseLdapForAuthenticationDescription": "Pokud je zakázáno, hesla užibatelů budou uložena i v databázi Matomou. To zkrátí čas potřebný k přihlášení, ale není to tak bezpečné.",
+ "PasswordField": "Pole hesla uživatele",
+ "PasswordFieldDescription": "Jméno LDAP atributu obsahujícího heslo uživatele, např. 'userPassword' nebo 'unicodePwd'.",
+ "PasswordFieldDescription2": "Pokud je 'Vždy použít LDAP pro ověření' povoleno a 'Vygenerovat náhodný token_auth pro nové uživatele' zakázáno, pak musí být v LDAP poli hash nebo zašifrovaná podoba uživatelova hesla",
+ "ReadMoreAboutAccessSynchronization": "Pokud se chcete dozvědět více o synchronizaci uživatelů, %1$spřečtěte si naši dokumentaci%2$s.",
+ "ExpectedLdapAttributes": "Očekávané LDAP atributy",
+ "ExpectedLdapAttributesPrelude": "S touto konfigurací bude LoginLDAP očekávat LDAP atributy, které vypadají nějak takto",
+ "NetworkTimeout": "Časový limit pro LDAP požadavky (v sekundách)",
+ "NetworkTimeoutDescription": "Časový limit v sekundách pro LDAP síťový požadavek.",
+ "NetworkTimeoutDescription2": "Čím větší je tento limit, tím déle budou muset uživatelé před přihlášením čekat, pokud nebude jeden z LDAP serverů dostupný.",
+ "Go": "Jdi",
+ "LoginPluginEnabledWarning": "Je povolen zásuvný modul %1$s i %2$s. To způsobí, že přihlášení pro LDAP uživatele selže. Prosím, zakažte zásuvný modul %1$s.",
+ "MemberOfField": "LDAP pole memberOf",
+ "MemberOfFieldDescription": "Pole používané vaším LDAP k určení členství, ve výchozím stavu \"memberOf\"",
+ "LdapUrlPortWarning": "Možnost port je ignorována, pokud je jméno hostitele zadáno jako URL (t. j. ldap:\/\/localhost\/ místo localhost).",
+ "UpdateFromPre300Warning": "Protože jste aktualizovali z verze starší než 3.0, nejspíš byste měli nechat tuto možnost odškrtnutou. Před verzí 3.0 byla hesla uchovávána jak na LDAP serveru, tak v databázi Matomou. Tato možnost musí být odškrtnuta, nebo se uživatelé přidaní před aktualizací nebudou schopni přihlásit.",
+ "MobileAppIntegrationNote": "Poznámka: Pokud plánujete používat LDAP s oficiální mobilní aplikací, musí být tato možnost odškrtnuta. Mobilní aplikace zatím nedokáže ověřit uživatele, pokud nejsou hesla uložena v databázi Matomou.",
+ "PasswordFieldHelp": "Vložte heslo pro přepsání aktuálního hesla nebo ponechte prázdné pro ponechání beze změn.",
+ "LdapUserCantChangePassword": "Změna hesla není podporována pro LDAP uživatele. Používáte LDAP a vaše nastavení jsou spravována přímo v LDAP. Pro více informací prosím kontaktujte svého administrátora LDAP serveru nebo administrátora instance Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/da.json b/lang/da.json
new file mode 100644
index 0000000..87c2c97
--- /dev/null
+++ b/lang/da.json
@@ -0,0 +1,85 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Log ind udvidelse med LDAP-godkendelse. Giver et alternativ til den normale Matomo Log ind udvidelse.",
+ "MenuLdap": "LDAP-brugere",
+ "LoadUser": "Indlæs bruger fra LDAP",
+ "LoadUserDescription": "Brug formularen til at synkronisere en enkelt bruger i LDAP fra brugergrænseflade.",
+ "LoadUserCommandDesc": "Synkronisere flere brugere med %s kommando.",
+ "Settings": "LDAP-indstillinger",
+ "AliasField": "Alias felt",
+ "BaseDn": "Basis DN",
+ "LdapPort": "Server port",
+ "MailField": "Mail felt",
+ "ServerUrl": "Server URL",
+ "UserIdField": "Bruger ID-felt",
+ "AliasFieldDescription": "( fx. \"cn\" )",
+ "MailFieldDescription": "( fx. \"mail\" )",
+ "UserIdFieldDescription": "( fx. \"uid\", AD: \"userPrincipalName\" )",
+ "UserNotFound": "Bruger \"%s\" blev ikke fundet!",
+ "NoUserName": "Intet brugernavn opgivet!",
+ "UsernameSuffix": "Brugernavn suffix\/domæne",
+ "UsernameSuffixDescription": "( fx. \"@localhost.com\" )",
+ "AdminUser": "LDAP Bind brugernavn",
+ "AdminUserDescription": "(fx. \"john\" - skal have læseadgang for at kunne forespørge på andre brugere)",
+ "AdminPass": "LDAP adgangskode",
+ "MemberOf": "ActiveDirectory gruppe",
+ "MemberOfDescription": "( fx. \"CN=Matomo User,CN=Users,DC=organization,DC=com\" )",
+ "MemberOfDescription2": "Bemærk: Denne indstilling bruger memberOf=? LDAP filter. Nogle LDAP-servere kræver ekstra opsætning for at dette virker.",
+ "Filter": "LDAP søgefilter",
+ "FilterDescription": "( fx. \"(objectClass=person)\" )",
+ "Kerberos": "Kerberos er aktiveret",
+ "LdapUserAdded": "LDAP bruger tilføjet!",
+ "LdapFunctionsMissing": "PHP ldap_connect() funktionen mangler, aktiver LDAP i PHP config!",
+ "CannotConnectToServer": "Kunne ikke forbinde til LDAP-serveren.",
+ "CannotConnectToServers": "Kunne ikke forbinde til nogen af ​​%s LDAP-serverne.",
+ "LDAPServers": "LDAP-servere",
+ "OneUser": "1 bruger",
+ "MemberOfCount": "%s er medlem af denne gruppe.",
+ "FilterCount": "%s matches af dette filter.",
+ "Test": "Test",
+ "InvalidFilter": "Ugyldigt filter.",
+ "FirstNameField": "Fornavn felt",
+ "FirstNameFieldDescription": "LDAP-attribut, der indeholder en brugers fornavn.",
+ "LastNameField": "Efternavn felt",
+ "LastNameFieldDescription": "LDAP-attribut, der indeholder en brugers efternavn.",
+ "FirstLastNameForAlias": "For- og efternavn anvendes til at generere et alias, hvis man ikke kan findes i LDAP.",
+ "NewUserDefaultSitesViewAccess": "Indledende websteder med læseadgang for nye brugere",
+ "NewUserDefaultSitesViewAccessDescription": "Hvis angivet, når en LDAP bruger synkroniseres for første gang, får man læseadgang til disse websteder. Skal være indstillet til en liste over id'er eller 'alle'.",
+ "EnableLdapAccessSynchronization": "Aktiver brugeradgang synkronisering fra LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Hvis aktiveret, bestemmes bruger adgangsniveauer af brugerdefinerede LDAP attributter. Bemærk: For at bruge denne funktion, skal LDAP serverens skema ændres.",
+ "LdapViewAccessField": "LDAP læse adgangsfelt",
+ "LdapViewAccessFieldDescription": "LDAP-attribut, der definerer, hvilke sider brugeren kan se.",
+ "LdapAdminAccessField": "LDAP Admin adgangsfelt",
+ "LdapAdminAccessFieldDescription": "Brugerdefineret LDAP-attribut, der bestemmer, hvilke websteder en bruger har admin adgang til.",
+ "LdapSuperUserAccessField": "LDAP superbruger adgangsfelt",
+ "LdapSuperUserAccessFieldDescription": "Brugerdefineret LDAP-attribut, der bestemmer, om en bruger er superbruger eller ej.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Afgrænsningstegn for serverspecifikation brugeradgangsattribut",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Strengen bruges til at afgrænse server specifikationer i en brugeradgangsattribut. Hvis sat til ';', vil attributadgangen forventes at ligne '<server-spec>;<server-spec>;... '.",
+ "LdapUserAccessAttributeServerSeparator": "Brugeradgang attributserver & webside separator",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Strengen bruges til at adskille Matomo serverforekomst id'er med webstedets ID lister. Hvis den er sat til ':', forventes adgangsattributten at ligne '<server-id>:<site-list>;<server-id>:<site-list> '.",
+ "ThisMatomoInstanceName": "Specielt navn for Matomo instansen",
+ "ThisMatomoInstanceNameDescription": "Et særligt navn, der bruges til at identificere Matomo instansen i en adgangsattribut. Hvis den ikke er angivet, forventes basis URL for Matomo installationen.",
+ "UnsupportedPasswordReset": "Kan ikke nulstille brugerens adgangskode, kontakt en administrator.",
+ "UserSyncSettings": "Bruger synkroniseringsindstillinger",
+ "AccessSyncSettings": "Adgang til synkroniseringsindstillinger",
+ "SynchronizeUsersAfterLogin": "Synkroniser brugere efter gennemført log ind",
+ "SynchronizeUsersAfterLoginDescription": "Hvis aktiveret, vil brugernes detaljer i LDAP altid synkroniseres efter en vellykket logind.",
+ "SynchronizeUsersAfterLoginDescription2": "Hvis deaktiveret, vil plugin ikke synkronisere brugeroplysninger efter log ind, medmindre LDAP-godkendelse er aktiveret. Når det er muligt, vil LDAP-forbindelser helt undgås.",
+ "UseLdapForAuthentication": "Brug altid LDAP til godkendelse",
+ "UseLdapForAuthenticationDescription": "Hvis deaktiveret, vil brugernes adgangskode gemmes i Matomo DB foruden LDAP. Dette vil resultere i hurtigere logind, men er mindre sikker.",
+ "PasswordField": "Bruger adgangskodefelt",
+ "PasswordFieldDescription": "Navn på LDAP-attribut, der indeholder en brugers adgangskode, fx 'userPassword' eller 'unicodePwd'.",
+ "ReadMoreAboutAccessSynchronization": "Lære mere om synkronisering af brugeradgang ,%1$slæs dokumentation%2$s.",
+ "ExpectedLdapAttributes": "Forventede LDAP attributter",
+ "ExpectedLdapAttributesPrelude": "Med denne konfiguration vil LoginLdap forvente attributter i LDAP der ser ud som",
+ "NetworkTimeout": "LDAP netværk anmodning timeout (i sek)",
+ "NetworkTimeoutDescription": "Tidsfristen i sekunder for en LDAP netværksanmodning .",
+ "NetworkTimeoutDescription2": "Jo større timeout, jo længere skal dine brugere vente, før de kan logge på Matomo via LDAP, hvis en eller flere af din servere ikke returnerer noget svar.",
+ "Go": "Start",
+ "LoginPluginEnabledWarning": "Både %1$s og %2$s udvidelserne er slået til! Dette vil medføre at logind for LDAP-brugere vil mislykkes, deaktiver %1$s udvidelsen.",
+ "MemberOfField": "LDAP memberOf felt",
+ "MemberOfFieldDescription": "Felt bruges af LDAP til at angive medlemskab, standard \"memberOf\"",
+ "LdapUrlPortWarning": "Port indstilling ignoreres når serverens værtsnavn er indstillet til en LDAP URL (fx. ldap:\/\/localhost\/ i stedet for localhost).",
+ "MobileAppIntegrationNote": "Bemærk: Hvis det planlægges at bruge LDAP med den officielle mobile app, skal denne indstilling være markeret. I øjeblikket kan den mobile app ikke godkende brugere, hvis brugernes adgangskode ikke er gemt i Matomo databasen."
+ }
+} \ No newline at end of file
diff --git a/lang/de.json b/lang/de.json
new file mode 100644
index 0000000..cf58aea
--- /dev/null
+++ b/lang/de.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Login Plugin mit LDAP Authentifizierung als Alternative zum Standard Matomo Login Plugin.",
+ "MenuLdap": "LDAP Benutzer",
+ "LoadUser": "Benutzer aus LDAP laden",
+ "LoadUserDescription": "Sie können dieses Formular einsetzen um einen einzelnen Benutzer in LDAP per Benutzerschnittstelle zu synchronisieren.",
+ "LoadUserCommandDesc": "Um mehr Benutzer zu synchronisieren, benutzen Sie den %s Befehl.",
+ "Settings": "LDAP Einstellungen",
+ "AliasField": "Alias Feld",
+ "BaseDn": "Basis DN",
+ "LdapPort": "Server Port",
+ "MailField": "Mail Feld",
+ "ServerUrl": "Server-URL",
+ "UserIdField": "Benutzer ID Feld",
+ "AliasFieldDescription": "Das LDAP-Attribut, das verwendet wird, um den Alias des Benutzers in Matomo festzulegen (z.B..\"cn\")",
+ "MailFieldDescription": "( e.g. \"mail\" )",
+ "UserIdFieldDescription": "( e.g. \"uid\", AD: \"userPrincipalName\" )",
+ "UserNotFound": "Der Benutzer \"%s\" konnte nicht gefunden werden!",
+ "NoUserName": "Sie haben keinen Benutzernamen angegeben!",
+ "UsernameSuffix": "Benutzername Suffix \/ Domäne",
+ "UsernameSuffixDescription": "( e.g. \"@localhost.com\" )",
+ "AdminUser": "Administrator Benutzer",
+ "AdminUserDescription": "( e.g. \"Administrator\" )",
+ "AdminPass": "LDAP Passwort",
+ "MemberOf": "Benötigte Benutzergruppe",
+ "MemberOfDescription": "( e.g. \"CN=Matomo User,CN=Users,DC=organization,DC=com\" )",
+ "MemberOfDescription2": "Bemerkung: Diese Einstellung setzt den memberOf=? LDAP Filter ein. Einige LDAP Server benötigen eine zusätzliche Konfiguration um diese Abfrage ausführen zu können.",
+ "Filter": "LDAP Suchfilter",
+ "FilterDescription": "( e.g. \"(objectClass=person)\" )",
+ "Kerberos": "Verwende Web-Server-Authentifizierung (z.B. Kerberos SSO)",
+ "KerberosDescription": "Falls aktiviert prüft das Plugin die Variable $_SERVER['REMOTE_USER'] und nimmt an, dass der Benutzer bereits angemeldet ist. Falls die Variable $_SERVER['REMOTE_USER'] nicht vorhanden ist, werden Benutzer gemäß den anderen Einstellungen bestätigt.",
+ "LdapUserAdded": "LDAP Benutzer erfolgreich hinzugefügt!",
+ "LdapFunctionsMissing": "PHP ldap_connect() Funktion fehlt, aktivieren Sie bitte LDAP-Modul auf Ihrer PHP-Konfiguration!",
+ "CannotConnectToServer": "Es konnte keine Verbindung zum LDAP Server aufgebaut werden.",
+ "CannotConnectToServers": "Zu keinem der %s LDAP Server konnte eine Verbindung aufgebaut werden.",
+ "LDAPServers": "LDAP Server",
+ "OneUser": "1 Benutzer",
+ "MemberOfCount": "%s sind ein Mitglied dieser Gruppe.",
+ "FilterCount": "%s treffen auf diesen Filter zu.",
+ "Test": "Test",
+ "InvalidFilter": "Ungültiger Filter.",
+ "ServerName": "Server Name",
+ "FirstNameField": "Feld Vorname",
+ "FirstNameFieldDescription": "Die LDAP Eigenschaft enthält einen Vornamen für Benutzer.",
+ "LastNameField": "Feld Nachname",
+ "LastNameFieldDescription": "LDAP Attribut, das den Nachnamen des Benutzers enthält.",
+ "FirstLastNameForAlias": "Vor- und Nachname werden eingesetzt, um einen Alias zu generieren, wenn ein Benutzer in LDAP nicht gefunden werden kann.",
+ "NewUserDefaultSitesViewAccess": "Initiale Seiten mit Ansichtszugriff für neue Benutzer",
+ "NewUserDefaultSitesViewAccessDescription": "Wenn spezifiziert, wird zum ersten Mal synchronisierten Benutzern Lesezugriff für die Seiten gewährt. Sollte eine Liste mit IDs sein oder 'all' für alle.",
+ "EnableLdapAccessSynchronization": "Benutzerzugriff-Synchronisation von LDAP aktivieren",
+ "EnableLdapAccessSynchronizationDescription": "Wenn aktiviert, werden Benutzerzugriffsrechte durch LDAP Attribute definiert. Achtung: Die Anwendung dieses Features bedingt eine Korrektur des Schemas Ihres LDAP Servers.",
+ "LdapViewAccessField": "LDAP Feld Ansichtszugriff",
+ "LdapViewAccessFieldDescription": "LDAP Attribut das definiert, welche Seiten ein Benutzer ansehen darf.",
+ "LdapAdminAccessField": "LDAP Feld Administratorzugriff",
+ "LdapAdminAccessFieldDescription": "LDAP Attribut, das vorgibt, auf welche Seiten ein Benutzer Administratorenzugriff hat.",
+ "LdapSuperUserAccessField": "LDAP Feld Hauptadministrator Zugriff",
+ "LdapSuperUserAccessFieldDescription": "LDAP Attribut, das vorgibt, ob ein Benutzer Superuser ist oder nicht.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Trennzeichen für die Server Spezifikation des Benutzerzugriffsattributs.",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Das Trennzeichen um die Server Spezifikationen in einem Benutzerzugriffsattribut zu trennen. Wenn auf ';' gesetzt, wird ein Zugriffsattribut mit folgendem Aufbau erwartet: '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Benutzerzugriffs Eigenschaft Server- & Seitenlisten-Separator",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Die eingesetzte Zeichenfolge, welche separate Matomo Server Instanzen mit Seiten-Ids auflistet. Wenn auf ':' gesetzt, wird ein Zugriffsattribut wie folgt erwartet: '<server-id>:<site-list>;<server-id>:<site-list>'",
+ "ThisMatomoInstanceName": "Spezialname für diese Matomo Instanz",
+ "ThisMatomoInstanceNameDescription": "Ein spezieller Name, mit welchem diese Matomo Instanz in einem Zugriffsattribut identifiziert werden kann. Wenn keines spezifiziert ist, wird die Basis URL dieser Matomo Installation erwartet.",
+ "UnsupportedPasswordReset": "Das Benutzerpasswort kann nicht zurückgesetzt werden, bitte kontaktieren Sie einen Administrator.",
+ "UserSyncSettings": "Einstellungen für Benutzersynchronisation",
+ "AccessSyncSettings": "Synchronisationseinstellungen öffnen",
+ "SynchronizeUsersAfterLogin": "Benutzer nach erfolgreichem Login synchronisieren",
+ "SynchronizeUsersAfterLoginDescription": "Wenn angewählt, werden Benutzerdetails in LDAP immer synchronisiert, nachdem sich der Benutzer erfolgreich angemeldet hat.",
+ "SynchronizeUsersAfterLoginDescription2": "Wenn deaktiviert, wird das Plugin Benutzerinformationen nach erfolgreichem anmelden nicht synchronisieren, es sei denn, LDAP Authenifizierung ist angewählt. Wenn möglich werden LDAP Verbindungen komplett vermieden.",
+ "UseLdapForAuthentication": "Immer LDAP für die Authentifizierung einsetzen",
+ "UseLdapForAuthenticationDescription": "Wenn deaktiviert, werden Benutzerpasswörter zusätzlich zu LDAP in der Matomo Datenbank gespeichert. Das führt zu einer schnelleren Anmeldung, aber ist weniger sicher.",
+ "PasswordField": "Feld Benutzerpasswort",
+ "PasswordFieldDescription": "Name der LDAP Eigenschaft, welche das Benutzerpasswort enthält, z. B. 'userPassword' or 'unicodePwd'.",
+ "PasswordFieldDescription2": "Wenn 'Immer LDAP für die Authentifizierung einsetzen' angewählt und 'Für neue Benutzer zufälligen token_auth generieren' deaktiviert ist, muss der Wert dieses Feldes in LDAP ein gehashtes oder verschlüsseltes Benutzerpasswort sein.",
+ "ReadMoreAboutAccessSynchronization": "Um mehr über Benutzerzugriffssynchronisation zu lernen, %1$slesen Sie unsere Dokumentation%2$s.",
+ "ExpectedLdapAttributes": "Erwartete LDAP Eigenschaften",
+ "ExpectedLdapAttributesPrelude": "Mit dieser Konfiguration wird LoginLdap Einstellungen in LDAP erwarten, die aussehen wie",
+ "NetworkTimeout": "LDAP Netzwerk Abfrage Timeout (in Sekunden)",
+ "NetworkTimeoutDescription": "Das Zeitlimit in Sekunden für eine LDAP Netzwerkanfrage.",
+ "NetworkTimeoutDescription2": "Je grösser das Timeout, desto länger müssen Ihre Benutzer warten, bevor Matomo über LDAP loggen kann, wenn einer oder mehrere Ihrer Server keine Antwort mehr liefert.",
+ "Go": "Los",
+ "LoginPluginEnabledWarning": "Sowohl das %1$s als auch das %2$s Plugin ist aktiviert! Das wird zu fehlgeschlagenen Anmeldeversuchen von LDAP Benutzern führen. Bitte das %1$s Plugin deaktivieren.",
+ "MemberOfField": "LDAP memberOf Feld",
+ "MemberOfFieldDescription": "Von Ihrem LDAP benütztes Feld um die Mitgliedschaft anzuzeigen, per Standard \"memberOf\"",
+ "LdapUrlPortWarning": "Die Port Einstellung wird ignoriert, wenn der Server Hostname auf eine LDAP URL (zum Beispiel ldap:\/\/localhost\/ anstelle von localhost) gesetzt ist.",
+ "UpdateFromPre300Warning": "Da Sie von einer Version älter als 3.0.0 aktualisieren, sollten Sie diese Option höchstwahrscheinlich ungewählt belassen. Vor der Version 3.0.0 wurden Passwörter sowohl in LDAP als auch in der Matomo Datenbank gespeichert. Diese Option muss abgewählt sein damit vor dem Update hinzugefügte Benutzer weiterhin in der Lage sind, sich anzumelden.",
+ "MobileAppIntegrationNote": "Hinweis: Wenn Sie vorhaben, LDAP mit der offiziellen Mobile App einzusetzen, darf diese Option nicht angewählt sein. Aktuell kann die Mobile App Benutzer nicht authentifizieren wenn deren Passwort nicht in der Matomo Datenbank gespeichert ist.",
+ "PasswordFieldHelp": "Geben Sie ein Passwort ein, um das existierende Passwort zu überschreiben oder lassen Sie das Feld leer um keine Änderung vorzunehmen.",
+ "LdapUserCantChangePassword": "Eine Kennwortänderung ist für LDAP-Benutzer nicht möglich, da die Benutzereinstellungen von einem LDAP-Server verwaltet werden. Wenden Sie sich in diesem Fall an Ihren LDAP-Serveradministrator oder Ihren Matomo-Administrator, um weitere Hilfe zu erhalten."
+ }
+} \ No newline at end of file
diff --git a/lang/el.json b/lang/el.json
new file mode 100644
index 0000000..c1d8fcd
--- /dev/null
+++ b/lang/el.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Πρόσθετο για είσοδο με αυθεντικοποίηση LDAP. Παρέχει μια εναλλακτική μέθοδο στην στάνταρ είσοδο για το Matomo.",
+ "MenuLdap": "Χρήστες LDAP",
+ "LoadUser": "Να φορτωθεί ο χρήστης από LDAP",
+ "LoadUserDescription": "Χρησιμοποιήστε αυτή τη φόρμα για συγχρονισμό ενός χρήστη στο LDAP μέσα από το γραφικό περιβάλλον.",
+ "LoadUserCommandDesc": "Για τον συγχρονισμό περισσότερων χρηστών, χρησιμοποιήστε την εντολή %s.",
+ "Settings": "Ρυθμίσεις LDAP",
+ "AliasField": "Πεδίο ψευδωνύμου",
+ "BaseDn": "Βασικό DN",
+ "LdapPort": "Θύρα διακομιστή",
+ "MailField": "Πεδίο για ηλεκτρ. ταχυδρομείο",
+ "ServerUrl": "Διεύθυνση URL διακομιστή",
+ "UserIdField": "Πεδίο αναγνωριστικού χρήστη",
+ "AliasFieldDescription": "Η ιδιότητα LDAP που θα χρησιμοποιείται για τη δημιουργία ενός ψευδώνυμου χρήστη στο Matomo, πχ. \"cn\".",
+ "MailFieldDescription": "( πχ. \"mail\" )",
+ "UserIdFieldDescription": "( πχ. \"uid\", AD: \"userPrincipalName\" )",
+ "UserNotFound": "Δε βρέθηκε ο χρήστης \"%s\"!",
+ "NoUserName": "Δε δόθηκε όνομα χρήστη!",
+ "UsernameSuffix": "Επίθεμα ονόματος χρήστη \/ Τομέας",
+ "UsernameSuffixDescription": "( πχ. \"@localhost.com\" )",
+ "AdminUser": "Όνομα χρήστη σύνδεσης LDAP",
+ "AdminUserDescription": "(πχ. \"john\" - πρέπει να έχει δικαίωμα ανάγνωσης για να κάνει ερωτήματα για άλλους χρήστες)",
+ "AdminPass": "Συνθηματικό LDAP",
+ "MemberOf": "Ομάδα ActiveDirectory",
+ "MemberOfDescription": "Ένα γκρουπ του οποίου ο χρήστης πρέπει να είναι μέλος πχ. \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "Σημείωση: Η ρύθμιση αυτή χρησιμοποιεί το φίλτρο memberOf=? του LDAP. Ορισμένοι διακομιστές LDAP απαιτούν επιπλέον παραμετροποίηση για να δουλέψει αυτό.",
+ "Filter": "Φίλτρο αναζήτησης LDAP",
+ "FilterDescription": "( πχ. \"(objectClass=person)\" )",
+ "Kerberos": "Το Kerberos ενεργοποιήθηκε",
+ "KerberosDescription": "Αν είναι ενεργοποιημένο, το πρόσθετο θα ελέγξει τη μεταβλητή $_SERVER['REMOTE_USER'] και θα υποθέσει ότι ο χρήστης είναι ήδη πιστοποιημένος. Αν η μεταβλητή $_SERVER['REMOTE_USER'] δεν υπάρχει, όλοι οι χρήστες θα είναι πιστοποιημένοι βάσει άλλων παραμέτρων.",
+ "LdapUserAdded": "Η προσθήκη του χρήστη LDAP έγινε με επιτυχία!",
+ "LdapFunctionsMissing": "Η συνάρτηση PHP ldap_connect() δεν υπάρχει, παρακαλούμε ενεργοποιήστε το LDAP στην παραμετροποίηση PHP!",
+ "CannotConnectToServer": "Δεν ήταν δυνατή η σύνδεση με τον LDAP διακομιστή.",
+ "CannotConnectToServers": "Δεν ήταν δυνατή η σύνδεση με κάποιον από τους %s διακομιστές LDAP.",
+ "LDAPServers": "Διακομιστές LDAP",
+ "OneUser": "1 χρήστης",
+ "MemberOfCount": "Ο %s είναι μέλος αυτής της ομάδας.",
+ "FilterCount": "%s ταιριάζουν με το φίλτρο.",
+ "Test": "Έλεγχος",
+ "InvalidFilter": "Άκυρο φίλτρο.",
+ "ServerName": "Όνομα διακομιστή",
+ "FirstNameField": "Πεδίο ονόματος",
+ "FirstNameFieldDescription": "Το όρισμα LDAP που περιέχει το όνομα του χρήστη.",
+ "LastNameField": "Πεδίο επωνύμου",
+ "LastNameFieldDescription": "Το όρισμα LDAP που περιέχει το επώνυμο του χρήστη.",
+ "FirstLastNameForAlias": "Το όνομα και επίθετο χρησιμοποιούνται για τη δημιουργία ενός ψευδωνύμου αν δεν μπορεί να βρεθεί ένα στο LDAP.",
+ "NewUserDefaultSitesViewAccess": "Αρχικοί Ιστοτόποι για Πρόσβαση Ανάγνωσης για τους Νέους Χρήστες",
+ "NewUserDefaultSitesViewAccessDescription": "Αν οριστεί, όταν ένας χρήστης LDAP συγχρονίζεται για την πρώτη φορά, του δίνεται πρόσβαση στους ιστοτόπους αυτούς. Θα πρέπει να οριστεί σε μια λίστα αναγνωριστικών ή 'all'.",
+ "EnableLdapAccessSynchronization": "Ενεργοποίηση του Συγχρονισμού Πρόσβασης Χρηστών από LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Αν είναι ενεργοποιημένο, τα επίπεδα πρόσβασης χρηστών καθορίζονται από ορίσματα του LDAP. Σημείωση: Για να χρησιμοποιήσετε αυτό το χαρακτηριστικό, θα πρέπει να αλλάξετε τις ρυθμίσεις σχήματος του διακομιστή σας LDAP.",
+ "LdapViewAccessField": "Πεδίο Πρόσβασης Ανάγνωσης του LDAP",
+ "LdapViewAccessFieldDescription": "Το οριζόμενο όρισμα του LDAP που καθορίζει σε ποιους ιστοτόπους ένα χρήστης θα έχει πρόσβαση για ανάγνωση.",
+ "LdapAdminAccessField": "Πεδίο Πρόσβασης Διαχειριστή LDAP",
+ "LdapAdminAccessFieldDescription": "Το οριζόμενο όρισμα στο LDAP που καθορίζει σε ποιους ιστοτόπους θα έχει διαχειριστική πρόσβαση ο χρήστης.",
+ "LdapSuperUserAccessField": "Πεδίο Πρόσβασης Υπερ Χρήστη στο LDAP",
+ "LdapSuperUserAccessFieldDescription": "Το οριζόμενο όρισμα στο LDAP που θα καθορίζει αν ένας χρήστης είναι υπερχρήστης ή όχι.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Ορισμός Διαχωριστή Ορίσματος Πρόσβασης Χρήστη Διακομιστή",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Το αλφαριθμητικό που χρησιμοποιείται για το διαχωρισμό των προδιαγραφών διακομιστών σε ένα όρισμα πρόσβασης χρήστη. Αν οριστεί σε ';', το όρισμα πρόσβασης πρέπει να έχει τη μορφή '<προδιαγραφή-διακομιστή>;προδιαγραφή-διακομιστή;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Όρισμα Πρόσβασης Χρήστη Διακομιστή & Διαχωριστής Λίστας Ιστοτόπων",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Το αλφαριθμητικό που χρησιμοποιείται για το διαχωρισμό των αναγνωριστικών στιγμιοτύπων του Matomo με τις λίστες αναγνωριστικών ιστοτόπων. Αν οριστεί σε ':', το όρισμα πρόσβασης πρέπει να έχει τη μορφή '<server-id>:<site-list>:<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Ειδικό Όνομα για αυτό το Στιγμιότυπο Matomo",
+ "ThisMatomoInstanceNameDescription": "Ένα ειδικό όνομα για να αναγνωρίζεται αυτό το στιγμιότυπο του Matomo σε ένα όρισμα πρόσβασης. Αν δεν οριστεί κάποιο, αναμένεται το βασικό τμήμα της διεύθυνσης URL του Matomo.",
+ "UnsupportedPasswordReset": "Δεν ήταν δυνατή η αρχικοποίηση του συνθηματικού του χρήστη, παρακαλούμε επικοινωνήστε με το διαχειριστή.",
+ "UserSyncSettings": "Ρυθμίσεις Συγχρονισμού Χρήστη",
+ "AccessSyncSettings": "Ρυθμίσεις Συγχρονισμού Πρόσβασης",
+ "SynchronizeUsersAfterLogin": "Συγχρονισμός των χρηστών μετά την επιτυχημένη είσοδο",
+ "SynchronizeUsersAfterLoginDescription": "Αν είναι ενεργοποιημένο, οι λεπτομέρειες χρήστη στο LDAP θα συγχρονίζονται κάθε φορά μετά από επιτυχημένη είσοδο.",
+ "SynchronizeUsersAfterLoginDescription2": "Αν είναι απενεργοποιημένο, το πρόσθετο δε θα συγχρονίζει τις πληροφορίες χρήστη μετά την είσοδο, εκτός αν είναι ενεργή η αυθεντικοποίηση από LDAP. Αν είναι δυνατόν, οι συνδέσεις LDAP θα αποφεύγονται εντελώς.",
+ "UseLdapForAuthentication": "Να χρησιμοποιείται πάντα το LDAP για αυθεντικοποίηση",
+ "UseLdapForAuthenticationDescription": "Αν είναι απενεργοποιημένο, τα συνθηματικά χρηστών θα αποθηκεύονται στη βάση του Matomo παράλληλα με το LDAP. Αυτό θα έχει ως αποτέλεσμα γρηγορότερη είσοδο, αλλά λιγότερη ασφάλεια.",
+ "PasswordField": "Πεδίο Συνθηματικού Χρήστη",
+ "PasswordFieldDescription": "Όνομα του ορίσματος LDAP που περιέχει το συνθηματικό του χρήστη, πχ. 'userPassword' ή 'unicodePwd'.",
+ "PasswordFieldDescription2": "Αν το 'Πάντα χρήση του LDAP για αυθεντικοποίηση' είναι ενεργό και το 'Δημιουργία Τυχαίου token_auth για Νέους Χρήστες' ανενεργό, η τιμή του πεδίου αυτού για το LDAP πρέπει να είναι ένα κατακερματισμένο ή κρυπτογραφημένο συνθηματικό.",
+ "ReadMoreAboutAccessSynchronization": "Για να μάθετε περισσότερα σχετικά με το συγχρονισμό πρόσβασης χρηστών, %1$sδιαβάστε την τεκμηρίωση%2$s.",
+ "ExpectedLdapAttributes": "Αναμενόμενα ορίσματα LDAP",
+ "ExpectedLdapAttributesPrelude": "Με τη ρύθμιση αυτή, το LoginLdap αναμένει ορίσματα στο LDAP όπως",
+ "NetworkTimeout": "Όριο χρόνου (σε δευτ.) αίτησης δικτύου για το LDAP",
+ "NetworkTimeoutDescription": "Το χρονικό όριο σε δευτερόλεπτα για μια δικτυακή αίτηση LDAP.",
+ "NetworkTimeoutDescription2": "Όσο μεγαλύτερο το χρονικό όριο, τόσο περισσότερο θα πρέπει οι χρήστες να περιμένουν προτού το Matomo τους επιτρέψει την είσοδο μέσω LDAP, αν ένας ή περισσότεροι διακομιστές σας δεν ανταποκρίνονται.",
+ "Go": "Μετάβαση",
+ "LoginPluginEnabledWarning": "Και το %1$s και το %2$s πρόσθετο είναι ενεργοποιημένα! Αυτό θα κάνει την είσοδο για τους χρήστες LDAP να αποτυγχάνει, παρακαλούμε απενεργοποιήστε το πρόσθετο %1$s.",
+ "MemberOfField": "Πεδίο memberOf του LDAP",
+ "MemberOfFieldDescription": "Πεδίο που χρησιμοποιείται από το LDAP που υποδεικνύει την ύπαρξη σε ομάδα, εξ' ορισμού το \"memberOf\"",
+ "LdapUrlPortWarning": "Η ρύθμιση θύρας αγνοείται όταν το όνομα διακομιστή ορίζεται σε LDAP URL (πχ. ldap:\/\/localhost\/ αντί του localhost).",
+ "UpdateFromPre300Warning": "Από τη στιγμή που κάνατε ενημέρωση από μια έκδοση πριν την 3.0.0, θα πρέπει να έχετε την επιλογή αυτή μη τσεκαρισμένη. Πριν την έκδοση 3.0.0, τα συνθηματικά των χρηστών αποθηκεύονταν και στο LDAP και στη βάση δεδομένων του Matomo. Η επιλογή αυτή πρέπει να μην είναι τσεκαρισμένη ώστε οι χρήστες που προστέθηκαν πριν την ενημέρωση να μπορούν να κάνουν είσοδο.",
+ "MobileAppIntegrationNote": "Σημείωση: Αν έχετε σκοπό να χρησιμοποιήσετε LDAP με την επίσημη εφαρμογή για κινητά, θα πρέπει να αφήσετε την επιλογή αυτή μη τσεκαρισμένη. Αυτή τη στιγμή, η εφαρμογή για κινητά δεν μπορεί να κάνει αυθεντικοποίηση χρηστών αν τα συνθηματικά χρηστών δεν είναι αποθηκευμένα στη βάση του Matomo.",
+ "PasswordFieldHelp": "Εισάγετε ένα κωδικό για να αντικαταστήσετε τον υπάρχων ή αφήστε το κενό για να μην κάνετε αλλαγή.",
+ "LdapUserCantChangePassword": "Η αλλαγή συνθηματικού δεν υποστηρίζεται για χρήστες LDAP. Αν χρησιμοποιείτε LDAP, οι ρυθμίσεις χρήστη διαχειρίζονται κατευθείαν από το LDAP. Για περισσότερες πληροφορίες, επικοινωνήστε με τον διαχειριστή του LDAP σας ή τον διαχειριστή του Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/en.json b/lang/en.json
new file mode 100644
index 0000000..39eff70
--- /dev/null
+++ b/lang/en.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Login plugin with LDAP authentication. Provides an alternative to the standard Matomo Login plugin.",
+ "MenuLdap": "LDAP Users",
+ "LoadUser": "Load User from LDAP",
+ "LoadUserDescription": "You can use this form to synchronize a single user in LDAP from the UI.",
+ "LoadUserCommandDesc": "To synchronize more users, use the %s command.",
+ "Settings": "LDAP Settings",
+ "AliasField": "User Alias Field",
+ "BaseDn": "Base DN",
+ "LdapPort": "Server Port",
+ "MailField": "E-mail Address Field",
+ "ServerUrl": "Server URL",
+ "UserIdField": "User ID Field",
+ "AliasFieldDescription": "The LDAP attribute used to generate a user's Matomo alias, e.g. \"cn\".",
+ "MailFieldDescription": "The LDAP attribute containing a user's e-mail address, e.g. \"mail\".",
+ "UserIdFieldDescription": "The LDAP attribute containing a user's username, e.g. \"uid\", or for Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "User \"%s\" not found!",
+ "NoUserName": "No username given!",
+ "UsernameSuffix": "E-mail Address Suffix",
+ "UsernameSuffixDescription": "Appended to usernames to generate e-mail addresses for users that have none in LDAP. Set to, e.g., \"@localhost.com\".",
+ "AdminUser": "LDAP Bind Username",
+ "AdminUserDescription": "User with access to other user entries, or blank for anonymous bind.",
+ "AdminPass": "LDAP Password",
+ "MemberOf": "Required User Group",
+ "MemberOfDescription": "A group the user is required to be a member of, e.g. \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "Note: This setting uses the memberOf=? LDAP filter. Some LDAP servers require extra setup for this to work.",
+ "Filter": "LDAP Search Filter",
+ "FilterDescription": "An LDAP filter used to determine which entities are allowed to be used for authentication, e.g. \"(objectClass=person)\".",
+ "Kerberos": "Use Web Server Auth (e.g. Kerberos SSO)",
+ "KerberosDescription": "If enabled, the plugin will check the $_SERVER['REMOTE_USER'] variable and assume the user is already authenticated. If the $_SERVER['REMOTE_USER'] is not present, all users are authenticated according to the other settings.",
+ "LdapUserAdded": "LDAP user added successfully!",
+ "LdapFunctionsMissing": "The PHP LDAP extension does not appear to be enabled. It is required for this plugin, please install it.",
+ "CannotConnectToServer": "Could not connect to the LDAP server.",
+ "CannotConnectToServers": "Could not connect to any of the %s LDAP servers.",
+ "LDAPServers": "LDAP Servers",
+ "OneUser": "1 user",
+ "MemberOfCount": "%s are a member of this group.",
+ "FilterCount": "%s are matched by this filter.",
+ "Test": "Test",
+ "InvalidFilter": "Invalid filter.",
+ "ServerName": "Server name",
+ "FirstNameField": "First Name Field",
+ "FirstNameFieldDescription": "The LDAP attribute containing a user's first name.",
+ "LastNameField": "Last Name Field",
+ "LastNameFieldDescription": "The LDAP attribute containing a user's last name.",
+ "FirstLastNameForAlias": "The first and last name are used to generate an alias if one cannot be found in LDAP.",
+ "NewUserDefaultSitesViewAccess": "Initial Sites With View Access For New Users",
+ "NewUserDefaultSitesViewAccessDescription": "If specified, when an LDAP user is synchronized for the first time (s)he is given view access to these sites. Should be set to a list of IDs or 'all'.",
+ "EnableLdapAccessSynchronization": "Enable User Access Synchronization from LDAP",
+ "EnableLdapAccessSynchronizationDescription": "If enabled, user access levels are determined by custom LDAP attributes. Note: To use this feature you will have to modify your LDAP server's schema.",
+ "LdapViewAccessField": "LDAP View Access Field",
+ "LdapViewAccessFieldDescription": "The custom LDAP attribute that determines which sites a user has view access to.",
+ "LdapAdminAccessField": "LDAP Admin Access Field",
+ "LdapAdminAccessFieldDescription": "The custom LDAP attribute that determines which sites a user has admin access to.",
+ "LdapSuperUserAccessField": "LDAP Super User Access Field",
+ "LdapSuperUserAccessFieldDescription": "The custom LDAP attribute that determines whether a user is a superuser or not.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "User Access Attribute Server Specification Delimiter",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "The string used to delimit server specifications in a user access attribute. If set to ';', the access attribute will be expected to look like '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "User Access Attribute Server & Site List Separator",
+ "LdapUserAccessAttributeServerSeparatorDescription": "The string used to separate Matomo server instance IDs with site ID lists. If set to ':', the access attribute will be expected to look like '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Special Name For This Matomo Instance",
+ "ThisMatomoInstanceNameDescription": "A special name used to identify this Matomo instance in an access attribute. If none is specified, we expect the base URL of this Matomo.",
+ "UnsupportedPasswordReset": "Cannot reset this user's password, please contact an administrator.",
+ "UserSyncSettings": "User Synchronization Settings",
+ "AccessSyncSettings": "Access Synchronization Settings",
+ "SynchronizeUsersAfterLogin": "Synchronize Users After Successful Login",
+ "SynchronizeUsersAfterLoginDescription": "If enabled, user details in LDAP will always be synchronized after a successful login.",
+ "SynchronizeUsersAfterLoginDescription2": "If disabled, the plugin will not synchronize user info after login, unless LDAP authentication is enabled. When possible, LDAP connections will be avoided entirely.",
+ "UseLdapForAuthentication": "Always Use LDAP for Authentication",
+ "UseLdapForAuthenticationDescription": "If disabled, user passwords will be stored in Matomo's DB in addition to LDAP. This will result in faster logins, but is less secure.",
+ "PasswordField": "User Password Field",
+ "PasswordFieldDescription": "Name of the LDAP attribute containing a user's password, e.g. 'userPassword' or 'unicodePwd'.",
+ "PasswordFieldDescription2": "If 'Always Use LDAP for Authentication' is enabled and 'Generate Random token_auth For New Users' is disabled, the value of this field in LDAP must be a user's hashed or encrypted password.",
+ "ReadMoreAboutAccessSynchronization": "To learn more about user access synchronization, %1$sread our docs%2$s.",
+ "ExpectedLdapAttributes": "Expected LDAP attributes",
+ "ExpectedLdapAttributesPrelude": "With this configuration, LoginLdap will expect attributes in LDAP that look like",
+ "NetworkTimeout": "LDAP Network Request Timeout (in secs)",
+ "NetworkTimeoutDescription": "The time limit in seconds for an LDAP network request.",
+ "NetworkTimeoutDescription2": "The larger the timeout, the more time your users may have to wait before Matomo can log them in through LDAP, if one or more of your servers becomes unresponsive.",
+ "Go": "Go",
+ "LoginPluginEnabledWarning": "Both the %1$s and %2$s plugins are enabled! This will cause logins for LDAP users to fail, please disable the %1$s plugin.",
+ "MemberOfField": "LDAP memberOf Field",
+ "MemberOfFieldDescription": "Field used by your LDAP to indicate membership, by default \"memberOf\"",
+ "LdapUrlPortWarning": "The port option is ignored when the server hostname is set to an LDAP URL (ie, ldap://localhost/ instead of localhost).",
+ "UpdateFromPre300Warning": "Since you updated from a pre-3.0.0 version, you should probably keep this option unchecked. Prior to version 3.0.0, user passwords were stored both in LDAP and in the Matomo DB. This option must be unchecked so that users added before the upgrade will be able to login.",
+ "MobileAppIntegrationNote": "Note: If you plan on using LDAP with the official mobile app, you must keep this option unchecked. Currently, the mobile app cannot authenticate users if user passwords are not stored in Matomo's DB.",
+ "PasswordFieldHelp": "Enter a password to overwrite the existing password or leave blank to make no change.",
+ "LdapUserCantChangePassword": "Changing your password is not supported for LDAP users. As you use LDAP, your user settings are managed in LDAP directly. For more information, please contact your LDAP server administrator or your Matomo administrator."
+ }
+}
diff --git a/lang/es.json b/lang/es.json
new file mode 100644
index 0000000..68ff793
--- /dev/null
+++ b/lang/es.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Complemento de inicio de sesión con autenticación LDAP. Suministra una alternativa al complemento de inicio de sesión estándar Matomo.",
+ "MenuLdap": "Usuarios LDAP",
+ "LoadUser": "Cargar usuario desde LDAP",
+ "LoadUserDescription": "Puede utilizar este formulario para sincronizar un usuario en LDAP desde la UI.",
+ "LoadUserCommandDesc": "Para sincronizar más usuarios, use el comando %s.",
+ "Settings": "Configuración LDAP",
+ "AliasField": "Campo de alias de usuario",
+ "BaseDn": "Base DN",
+ "LdapPort": "Puerto del servidor",
+ "MailField": "Campo de dirección de correo electrónico",
+ "ServerUrl": "URL del servidor",
+ "UserIdField": "Campo ID de usuario",
+ "AliasFieldDescription": "El atributo LDAP utilizado para generar el alias de usuario Matomo, p. ej. \"cn\".",
+ "MailFieldDescription": "El atributo LDAP conteniendo la dirección de correo electrónico de un usuario, p. ej. \"correo\".",
+ "UserIdFieldDescription": "El atributo LDAP conteniendo el nombre de usuario de un usuario, p. ej. \"uid\", o de un Directorio Activo: \"NombrePrincipalusuario\".",
+ "UserNotFound": "¡Usuario \"%s\" no encontrado!",
+ "NoUserName": "¡No se proporcionó nombre de usuario!",
+ "UsernameSuffix": "Sufijo de dirección de correo electrónico",
+ "UsernameSuffixDescription": "Adjuntado a los nombres de usuarios para generar direcciones de correo electrónico para usuarios que no poseen en LDAP. Configúrelo así, p. ej., \"@localhost.com\".",
+ "AdminUser": "Nombre de usuario de enlace LDAP",
+ "AdminUserDescription": "Usuario con acceso a otras entradas de usuario, o en blanco por enlace anónimo.",
+ "AdminPass": "Contraseña LDAP",
+ "MemberOf": "Grupo de usuario requerido",
+ "MemberOfDescription": "Un grupo en el que al usuario debe ser miembro de p. ej. \"cn=Usuario Matomo,cn=Usuarios,dc=organización,dc=com\".",
+ "MemberOfDescription2": "Nota: Esta configuración utiliza el filtro LDAP memberOf=?. Algunos servidores LDAP requieren de una configuración extra para que funcione.",
+ "Filter": "Filtro búsqueda LDAP",
+ "FilterDescription": "Un filtro LDAP utilizado para determinar qué entidades están permitidas para ser utilizadas a modo de autenticación, p. ej. \"(objectClass=persona)\".",
+ "Kerberos": "Usar autorización de servidor de internet (p. ej. Kerberos SSO)",
+ "KerberosDescription": "Si está habilitado, el complemento verificará la variable $_SERVER['REMOTE_USER'] y asumirá que el usuario ya está autentificado. Si $_SERVER['REMOTE_USER'] no está presente, todos los usuarios son autenticados de acuerdo a las otras configuraciones.",
+ "LdapUserAdded": "¡Usuario LDAP agregado exitosamente!",
+ "LdapFunctionsMissing": "La extensión PHP LDAP no parece estar habilitada. Es necesaria para este complemento, por favor instálela.",
+ "CannotConnectToServer": "No se pudo conectar con el servidor LDAP.",
+ "CannotConnectToServers": "No se pudo conectar con alguno de los servidores LDAP %s.",
+ "LDAPServers": "Servidores LDAP",
+ "OneUser": "1 usuario",
+ "MemberOfCount": "%s son miembros de este grupo.",
+ "FilterCount": "%s coinciden con este filtro.",
+ "Test": "Prueba",
+ "InvalidFilter": "Filtro inválido.",
+ "ServerName": "Nombre del servidor",
+ "FirstNameField": "Campo de nombre",
+ "FirstNameFieldDescription": "El atributo LDAP conteniendo el nombre del usuario.",
+ "LastNameField": "Campo de apellido",
+ "LastNameFieldDescription": "El atributo LDAP conteniendo el apellido del usuario.",
+ "FirstLastNameForAlias": "El nombre y apellido son utilizados para generar un alias si no puede ser encontrado en LDAP.",
+ "NewUserDefaultSitesViewAccess": "Sitios de internet iniciales con acceso de visualización para nuevos usuarios",
+ "NewUserDefaultSitesViewAccessDescription": "Si es especificado, cuando un usuario LDAP es sincronizado por primera vez el(la) le es otorgado poder observar estos sitios. Debe disponerse una lista de IDs o a 'todos'.",
+ "EnableLdapAccessSynchronization": "Habilitar acceso de sincronización al usuario desde LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Si es habilitado, los niveles de acceso del usuario están determinados por los atributos personalizados LDAP. Nota: Para utilizar esta función tendrá que modificar el esquema de sus servidores LDAP.",
+ "LdapViewAccessField": "Campo de acceso a la vista LDAP",
+ "LdapViewAccessFieldDescription": "El atributo personalizado LDAP que determina qué sitios de internet un usuario tiene acceso a su visualización.",
+ "LdapAdminAccessField": "Campo de acceso de administrador LDAP",
+ "LdapAdminAccessFieldDescription": "El atributo personalizado LDAP que determina a que sitios de internet un usuario posee accesos administrativos.",
+ "LdapSuperUserAccessField": "Campo de acceso de Super Usuario LDAP",
+ "LdapSuperUserAccessFieldDescription": "El atributo personalizado LDAP que determina si el usuario es un superusuario o no.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "\"User Access Attribute Server Specification Delimiter\"",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "La cadena utilizada para delimitar las especificaciones de servidor en un atributo de acceso para un usuario. Si es dispuesto en ';', el atributo de acceso esperado se verá como '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Atributo del servidor de acceso del usuario & separador de lista de sitios",
+ "LdapUserAccessAttributeServerSeparatorDescription": "La cadena utilizada para separar la instancias IDs del servidor Matomo con las listas ID del sitio. Si se dispone en ':', el atributo de acceso esperado se verá así '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Nombre especial para esta instancia de Matomo",
+ "ThisMatomoInstanceNameDescription": "Un nombre especial utilizado para identificar esta instancia Matomo con un atributo de acceso. Si no es especificado, esperamos la URL base de este Matomo.",
+ "UnsupportedPasswordReset": "No se puede restablecer la contraseña de usuario, por favor contáctese con un administrador.",
+ "UserSyncSettings": "Opciones de sincronización de usuario",
+ "AccessSyncSettings": "Configuraciones de sincronización de acceso",
+ "SynchronizeUsersAfterLogin": "Sincronizar usuarios después de un inicio de sesión exitoso",
+ "SynchronizeUsersAfterLoginDescription": "Si es habilitado, los detalles del usuario en LDAP se sincronizarán después de un unicio de sesión exitoso.",
+ "SynchronizeUsersAfterLoginDescription2": "Si es deshabilitado, el complemento no sincronizará la información del usuario después del inicio de sesión, a menos que la autenticación LDAP esté habilitada. Cuando sea posible, las conexiones LDAP serán totalmente evitadas.",
+ "UseLdapForAuthentication": "Siempre utilizar LDAP para autenticación",
+ "UseLdapForAuthenticationDescription": "Si es deshabilitado, las contraseñas de usuario serán guardadas en la base de datos de Matomo además de LDAP. Esto resultará en inicios de sesión más rápidos, pero menos seguros.",
+ "PasswordField": "Campo de contraseña",
+ "PasswordFieldDescription": "Nombre del atributo LDAP conteniendo la contraseña del usuario, p. ej.: 'userPassword' o 'unicodePwd'.",
+ "PasswordFieldDescription2": "Si 'Siempre utilizar LDAP para autenticación' está habilitado y 'Generar token_auth aleatorio para nuevos usuarios' está deshabilitado, el valor de este campo en LDAP debe ser un hash del usuario o contraseña encriptada.",
+ "ReadMoreAboutAccessSynchronization": "Para aprender más acerca de sincronización de acceso de usuario, %1$slea nuestra documentación%2$s.",
+ "ExpectedLdapAttributes": "Atributos LDAP esperados",
+ "ExpectedLdapAttributesPrelude": "Con esta configuración, LoginLdap esperará atributos en LDAP que se ven así",
+ "NetworkTimeout": "Solicitud de tiempo de respuesta de red LDAP (en segundos)",
+ "NetworkTimeoutDescription": "El límite de tiempo en segundos para una solicitud de red LDAP.",
+ "NetworkTimeoutDescription2": "Cuanto mayor el tiempo de estera, mayor es el tiempo que los usuarios pueden tener que esperar antes que Matomo pueda registrarlos a través de LDAP, si uno o más de sus servidores dejan de responder.",
+ "Go": "Ir",
+ "LoginPluginEnabledWarning": "Los complementos %1$s y %2$s están habilitados! Esto causará inicios de sesión de usuarios LDAP que fracasen, por favor deshabilite el complemento %1$s.",
+ "MemberOfField": "LDAP memberOf Field",
+ "MemberOfFieldDescription": "Campo utilizado por su LDAP para indicar membresía, de manera predeterminada \"memberOf\"",
+ "LdapUrlPortWarning": "La opción puerto es ignorada cuando el hostname del servidor se establece a una URL LDAP (ie, ldap:\/\/localhost\/ en lugar de localhost).",
+ "UpdateFromPre300Warning": "Desde que actualizó desde una versión pre-3.0.0, probablemente debería mantener esta opción sin marcar. Antes de la versión 3.0.0, las contraseñas de usuario se almacenaban tanto en LDAP y en la base de datos Matomo. Esta opción debe estar desactivada para que los usuarios agregados antes de la actualización puedan iniciar sesión.",
+ "MobileAppIntegrationNote": "Nota: Si planea utilizar LDAP con la aplicación móvil oficial, debe mantener esta opción deshabilitada. Actualmente, la aplicación móvil no puede autenticar usuarios si las contraseñas de los usuarios no están guardadas en la base de datos de Matomo.",
+ "PasswordFieldHelp": "Introduce una contraseña para sobreescribir la actual o deja en blanco para no hacer cambios.",
+ "LdapUserCantChangePassword": "Cambiando su contraseña no tiene soporte en usuarios LDAP. Si utiliza LDAP, su configuración es gestionada directamente desde LDAP. Para mayor información, por favor contáctese sea con su administrador de servidor LDAP o Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/et.json b/lang/et.json
new file mode 100644
index 0000000..1a49426
--- /dev/null
+++ b/lang/et.json
@@ -0,0 +1,33 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Käesolev plugin on alternatiiv originaalsele sisselogimisele, võimaldades kasutajaid autentida LDAP serveri vastu ja kasutada ka Kerberose tuge.",
+ "MenuLdap": "LDAP Kasutajad",
+ "LoadUser": "Lae kasutaja LDAP-ga",
+ "Settings": "LDAP seaded",
+ "AliasField": "Alias väli",
+ "BaseDn": "Baas otsingu DN",
+ "LdapPort": "Serveri port",
+ "MailField": "E-posti väli",
+ "ServerUrl": "Serveri URL",
+ "UserIdField": "Kasutaja ID väli",
+ "AliasFieldDescription": "( n. \"cn\" )",
+ "MailFieldDescription": "( n. \"mail\" )",
+ "UserIdFieldDescription": "( n. \"uid\", AD: \"userPrincipalName\" )",
+ "UserNotFound": "Kasutajat \"%s\" ei leitud!",
+ "NoUserName": "Kasutajanime pole täpsustatud!",
+ "UsernameSuffix": "Kasutajanime suffiks \/ domeen",
+ "UsernameSuffixDescription": "( n. \"@localhost.com\" )",
+ "AdminUser": "LDAP kasutajanimi",
+ "AdminUserDescription": "( n. \"john\" - serveris teiste kasutajate lugemisõigus )",
+ "AdminPass": "LDAP parool",
+ "MemberOf": "LDAP grupp",
+ "MemberOfDescription": "( n. \"CN=Matomo User,CN=Users,DC=organization,DC=com\" )",
+ "Filter": "LDAP otsingu filter",
+ "FilterDescription": "( n. \"(objectClass=person)\" )",
+ "Kerberos": "Kerberos aktiveeritud",
+ "LdapUserAdded": "LDAP kasutaja lisatud edukalt!",
+ "LdapFunctionsMissing": "PHP ldap_connect() funktsioon puudub, palun seadista LDAP moodul PHP konfiguratsioonis!",
+ "Test": "Testi",
+ "Go": "OK"
+ }
+} \ No newline at end of file
diff --git a/lang/eu.json b/lang/eu.json
new file mode 100644
index 0000000..ccda883
--- /dev/null
+++ b/lang/eu.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Probatu"
+ }
+} \ No newline at end of file
diff --git a/lang/fa.json b/lang/fa.json
new file mode 100644
index 0000000..c393b86
--- /dev/null
+++ b/lang/fa.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "آزمایش"
+ }
+} \ No newline at end of file
diff --git a/lang/fi.json b/lang/fi.json
new file mode 100644
index 0000000..ff32ac5
--- /dev/null
+++ b/lang/fi.json
@@ -0,0 +1,41 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Kirjautumislisäosa LDAP-autentikaatiolle. Tarjoaa vaihtoehdon Matomoin standardikirjautumiselle.",
+ "MenuLdap": "LDAP-käyttäjät",
+ "LoadUser": "Lataa käyttäjä LDAP:sta",
+ "Settings": "LDAP-asetukset",
+ "BaseDn": "Base DN",
+ "LdapPort": "Palvelimen portti",
+ "MailField": "Sähköpostiosoitteen kenttä",
+ "ServerUrl": "Palvelimen URL",
+ "UserIdField": "Käyttäjän ID -kenttä",
+ "AliasFieldDescription": "(esim. \"cn\")",
+ "UserNotFound": "Käyttäjää \"%s\" ei löydy!",
+ "NoUserName": "Ei käyttäjätunnusta!",
+ "UsernameSuffix": "Sähköpostiosoitteen pääte",
+ "AdminUser": "LDAP-käyttäjätunnus",
+ "AdminUserDescription": "(esim. \"john\" - täytyy olla lukuoikeus muiden käyttäjien tietoihin)",
+ "AdminPass": "LDAP-salasana",
+ "Filter": "LDAP:n hakufiltteri",
+ "FilterDescription": "( esim. \"(objectClass=person)\" )",
+ "Kerberos": "Kerberos käytössä",
+ "LdapUserAdded": "LDAP-käyttäjä lisättiin onnistuneesti!",
+ "CannotConnectToServer": "LDAP-palvelimeen yhdistäminen epäonnistui.",
+ "CannotConnectToServers": "%s LDAP-palvelimeen yhdistäminen epäonnistui.",
+ "LDAPServers": "LDAP-palvelimet",
+ "OneUser": "1 käyttäjä",
+ "Test": "Testi",
+ "InvalidFilter": "Virheellinen suodatin.",
+ "FirstNameField": "Etunimen kentän nimi",
+ "FirstNameFieldDescription": "LDAP-attribuutti käyttäjän etunimelle.",
+ "LastNameField": "Sukunimen kentän nimi",
+ "LastNameFieldDescription": "LDAP-attribuuttin käyttäjän sukunimelle.",
+ "UserSyncSettings": "Käyttäjien synkronointiasetukset",
+ "AccessSyncSettings": "Hallitse synkronointiasetuksia",
+ "PasswordField": "Käyttäjän salasanan kenttä",
+ "NetworkTimeout": "LDAP:n verkkoyhteyksien aikakatkaisu (sekunteina)",
+ "Go": "Mene",
+ "MemberOfField": "LDAPin \"memberOf\"-kenttä",
+ "PasswordFieldHelp": "Ylikirjoita nykyinen salasana. Jätä tyhjäksi pitääksesi vanhan asetuksen."
+ }
+} \ No newline at end of file
diff --git a/lang/fr.json b/lang/fr.json
new file mode 100644
index 0000000..e6310bd
--- /dev/null
+++ b/lang/fr.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Composant de connexion avec authentification LDAP, alternative au composant de connexion standard de Matomo.",
+ "MenuLdap": "Utilisateurs LDAP",
+ "LoadUser": "Charger l'utilisateur depuis le LDAP",
+ "LoadUserDescription": "Vous pouvez utiliser ce formulaire pour synchroniser le mot de passe d'un seul utilisateur LDAP depuis l'interface graphique.",
+ "LoadUserCommandDesc": "Pour synchroniser plus d'utilisateurs, utilisez la commande %s.",
+ "Settings": "Paramètres LDAP",
+ "AliasField": "Champ alias utilisateur",
+ "BaseDn": "DN de base",
+ "LdapPort": "Port du serveur LDAP",
+ "MailField": "Champ adresse e-mail",
+ "ServerUrl": "URL du serveur",
+ "UserIdField": "Champ ID utilisateur",
+ "AliasFieldDescription": "Attribut LDAP utilisé pour générer le nom complet d'utilisateur dans Matomo, par exemple \"cn\".",
+ "MailFieldDescription": "Attribut LDAP contenant l'adresse e-mail, par exemple \"mail\".",
+ "UserIdFieldDescription": "Attribut LDAP contenant le nom d'utilisateur, par exemple \"uid\", ou \"userPrincipalName\" pour un annuaire ActiveDirecotry.",
+ "UserNotFound": "Utilisateur \"%s\" introuvable !",
+ "NoUserName": "Pas de nom d'utilisateur trouvé.",
+ "UsernameSuffix": "Suffixe d'adresse e-mail",
+ "UsernameSuffixDescription": "Ajouté à l'alias pour générer l'adresse email si l'utilisateur n'en a aucun d'enregistrée dans le LDAP, par exemple : \"@localhost.com\".",
+ "AdminUser": "Login connexion LDAP",
+ "AdminUserDescription": "Utilisateur ayant accès aux autres entrées utilisateurs de l'annuaire. Laisser vide pour un accès anonyme.",
+ "AdminPass": "Mot de passe LDAP",
+ "MemberOf": "Groupe utilisateur requis",
+ "MemberOfDescription": "Nom du groupe dont l'utilisateur doit être membre pour pouvoir se connecter, par exemple : \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "NB : Ce paramètre utilise le filtre LDAP standard \"memberOf=?\". Certains serveurs LDAP nécessitent une configuration avancée pour cela.",
+ "Filter": "Filtre de recherche LDAP avancé",
+ "FilterDescription": "Filtre LDAP utilisé pour déterminer quelles entrées de l'annuaire sont autorisées à s'authentifier pour l'accès à Matomo. Par exemple : \"(objectClass=person)\".",
+ "Kerberos": "Utiliser Auth. Server Web (ex. Kerberos SSO)",
+ "KerberosDescription": "Si activé, ce composant va vérifier la variable $_SERVER['REMOTE_USER'] et supposer que l'utilisateur est déjà authentifié. Si $_SERVER['REMOTE_USER'] n'est pas présente, tous les utilisateurs seront authentifiés en fonction es autres paramètres.",
+ "LdapUserAdded": "Utilisateur LDAP ajouté avec succès !",
+ "LdapFunctionsMissing": "L'extension PHP LDAP, nécessaire au fonctionnement du composant n'est pas détectée. Merci de l'installer.",
+ "CannotConnectToServer": "Connexion au serveur LDAP impossible.",
+ "CannotConnectToServers": "Aucune connexion à l'un des %s serveurs LDAP n'est possible.",
+ "LDAPServers": "Serveur LDAP",
+ "OneUser": "1 utilisateur",
+ "MemberOfCount": "%s entrées dans le groupe.",
+ "FilterCount": "%s entrée correspondent au filtre.",
+ "Test": "Test",
+ "InvalidFilter": "Filtre invalide.",
+ "ServerName": "Nom du serveur",
+ "FirstNameField": "Champ Prénom",
+ "FirstNameFieldDescription": "Attribut LDAP contenant le prénom.",
+ "LastNameField": "Champ Nom",
+ "LastNameFieldDescription": "Attribut LDAP contenant le nom patronymique.",
+ "FirstLastNameForAlias": "Le prénom et le nom sont utilisés pour générer un nom complet si aucun nom complet n'est renseigné dans l'annuaire LDAP.",
+ "NewUserDefaultSitesViewAccess": "Sites initialement accessibles en consultation pour les nouveaux utilisateurs",
+ "NewUserDefaultSitesViewAccessDescription": "Sites auxquels un utilisateur du LDAP a accès en consultation lorsqu'il est synchronisé pour la première fois. Entrer la liste des ID des sites auxquels donner un accès en consultation ou utiliser 'all' pour tous.",
+ "EnableLdapAccessSynchronization": "Activer la synchronisation des droits d'accès utilisateurs depuis le LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Si cette option est activée, les droits d'accès utilisateurs sont déterminés par des attributs spécifiques du LDAP. NB : Pour utiliser cette fonction vous devez étendre préalablement votre schéma LDAP avec ces attributs spécifiques.",
+ "LdapViewAccessField": "Champ LDAP accès en consultation",
+ "LdapViewAccessFieldDescription": "Attribut LDAP qui détermine à quels sites l'utilisateur a un accès en consultation.",
+ "LdapAdminAccessField": "Champ LDAP accès administrateur",
+ "LdapAdminAccessFieldDescription": "Attribut LDAP qui détermine pour quels sites l'utilisateur dispose de droits administrateur.",
+ "LdapSuperUserAccessField": "Champ LDAP accès super-administrateur",
+ "LdapSuperUserAccessFieldDescription": "Attribut LDAP qui détermine si un utilisateur dispose des droits super-administrateur ou pas.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Séparateur entre les serveurs dans l'attribut accès utilisateur",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Chaîne de caractères utilisée pour séparer les différentes instances de Matomo dans l'attribut accès utilisateur. Si ';' est utilisé par exemple, l'attribut d'accès devrait ressembler à : '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Séparateur entre le serveur de la liste des sites dans l'attribut accès utilisateur",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Chaîne de caractères utilisée pour séparer l'ID de l'instance de Matomo ID de la liste des ID de sites. Par exemple, si ':' est utilisé, l'attribut d'accès devrait être renseigné ainsi : '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Nom spécifique pour cette instance Matomo",
+ "ThisMatomoInstanceNameDescription": "Nom spécifique utilisé pour identifier cette instance Matomo dans un attribut d'accès. Si rien n'est renseigné, nous cherchons l'URL de base de cette instance de Matomo.",
+ "UnsupportedPasswordReset": "Impossible de réinitialiser le mot de passe utiliseur. Merci de contacter votre administrateur.",
+ "UserSyncSettings": "Paramètres de synchronisation des utilisateurs",
+ "AccessSyncSettings": "Paramètres de synchronisation des accès",
+ "SynchronizeUsersAfterLogin": "Synchroniser les utilisateurs après une authentification réussie",
+ "SynchronizeUsersAfterLoginDescription": "Si cette option est activée, les informations de l'utilisateur seront synchronisées depuis le LDAP après chaque connection réussie.",
+ "SynchronizeUsersAfterLoginDescription2": "Si cette option est désactivée, le composant ne synchronisera pas les informations de l'utilisateur après connexion, même si l'authentification LDAP est activée. Si possible, les requêtes LDAP seront évitées totalement.",
+ "UseLdapForAuthentication": "Toujours utiliser LDAP pour l'authentification",
+ "UseLdapForAuthenticationDescription": "Si désactivé, les mots de passe utilisateur seront stockés dans la BDD Matomo en plus de LDAP. Cela permettra une authentification plus rapide mais c'est moins sécurisé.",
+ "PasswordField": "Champ mot de passe utilisateur",
+ "PasswordFieldDescription": "Attribut LDAP contenant le hash du mot de passe utilisateur, par exemple 'userPassword' ou 'unicodePwd'.",
+ "PasswordFieldDescription2": "Si \"toujours utiliser LDAP pour l'authentification\" est activé et \"générer des token_auth aléatoires pour les nouveaux utilisateurs\" est désactivé, la valeur de ce champs LDAP doit être un mot de passe utilisateur hashé ou bien chiffré.",
+ "ReadMoreAboutAccessSynchronization": "Pour en savoir plus sur la synchronisation des accès utilisateurs de Matomo, %1$slire la documentation%2$s.",
+ "ExpectedLdapAttributes": "Attributs LDAP attendus",
+ "ExpectedLdapAttributesPrelude": "Avec cette configuration, le composant LoginLdap attend des attributs LDAP qui ressemblent à ...",
+ "NetworkTimeout": "Délai d'attente LDAP (en secs)",
+ "NetworkTimeoutDescription": "Durée maximale d'attente en secondes pour une réponse à une requête LDAP.",
+ "NetworkTimeoutDescription2": "Supérieur au délai d'attente LDAP, temps maximum que vos utilisateurs peuvent avoir à attendre pour se connecter à Matomo via LDAP, si un ou plusieurs de vos serveurs LDAP ne répondent plus.",
+ "Go": "Aller",
+ "LoginPluginEnabledWarning": "Les composants %1$s et %2$s sont tous deux activés! Cela va causer un échec de l'authentification pour les utilisateurs LDAP, veuillez désactiver le composant %1$s.",
+ "MemberOfField": "Champ LDAP memberOf",
+ "MemberOfFieldDescription": "Champs utilisé par votre LDAP pour indiquer l'appartenance, par défaut \"memberOf\"",
+ "LdapUrlPortWarning": "L'option port est ignorée quand la valeur de nom d'hôte est définie à une URL LDAP (c'est à dire ldap:\/\/localhost\/ au lieu de localhost).",
+ "UpdateFromPre300Warning": "Puisque vous avez mis à jour depuis une version antérieure à 3.0.0, vous devriez probablement garder cette option non cochée. Avant la version 3.0.0 les mots de passe étaient stockés à la fois dans LDAP et dans la BDD Matomo. Cette option doit être décochée pour que les utilisateurs ajoutés à la mise à jour puissent continuer à s'authentifier.",
+ "MobileAppIntegrationNote": "Note: si vous planifiez d'utiliser LDAP avec l'application mobile officielle, vous devez garder cette option décochée. Actuellement l'application mobile ne peut pas authentifier des utilisateurs si leur mot de passe n'est pas stocké dans la BDD Matomo.",
+ "PasswordFieldHelp": "Entrez un mot de passe pour remplacer le mot de passe existant ou laisser vide pour ne rien changer.",
+ "LdapUserCantChangePassword": "La modification du mot de passe n'est pas disponible pour les utilisateurs authentifiés par LDAP. Utilisant LDAP, vous informations d'authentification sont gérées directement dans l'annuaire. Pour plus d'informations, contactez votre administrateur LDAP ou Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/he.json b/lang/he.json
new file mode 100644
index 0000000..a494361
--- /dev/null
+++ b/lang/he.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "בדיקה"
+ }
+} \ No newline at end of file
diff --git a/lang/hi.json b/lang/hi.json
new file mode 100644
index 0000000..5f5e073
--- /dev/null
+++ b/lang/hi.json
@@ -0,0 +1,30 @@
+{
+ "LoginLdap": {
+ "MenuLdap": "LDAP उपयोगकर्ताओं",
+ "LoadUser": "एलडीएपी से लोड उपयोगकर्ता",
+ "LoadUserDescription": "आप UIसे LDAP में एक भी उपयोगकर्ता सिंक्रनाइज़ करने के लिए इस प्रपत्र का उपयोग कर सकते हैं।",
+ "LoadUserCommandDesc": "अधिक उपयोगकर्ताओं को सिंक्रनाइज़ %s आदेश का उपयोग करें ।",
+ "Settings": "LDAP सेटिंग्स",
+ "AliasField": "प्रयोक्ता उर्फ ​​फील्ड",
+ "BaseDn": "बेस डी.एन.",
+ "LdapPort": "सर्वर पोर्ट",
+ "MailField": "ई-मेल पता क्षेत्र",
+ "ServerUrl": "सर्वर URL",
+ "UserIdField": "यूजर आईडी फील्ड",
+ "MailFieldDescription": "उदाहरण के लिए एक उपयोगकर्ता ई-मेल पते, जिसमें एलडीएपी विशेषता \"मेल\"।",
+ "UserNotFound": "उपयोगकर्ता \"%s\" को नहीं मिला!",
+ "NoUserName": "कोई उपयोगकर्ता नाम दिया!",
+ "UsernameSuffix": "ई-मेल पता प्रत्यय",
+ "AdminPass": "LDAP पासवर्ड",
+ "MemberOf": "आवश्यक उपयोगकर्ता समूह",
+ "Filter": "LDAP खोज फ़िल्टर",
+ "LdapUserAdded": "LDAP उपयोगकर्ता को सफलता पूर्वक जोड़ !",
+ "CannotConnectToServer": "LDAP सर्वर से कनेक्ट नहीं किया जा सका।",
+ "LDAPServers": "LDAP सर्वर",
+ "OneUser": "1 उपयोगकर्ता",
+ "Test": "परीक्षण",
+ "FirstNameField": "पहला नाम फील्ड",
+ "UserSyncSettings": "प्रयोक्ता तुल्यकालन सेटिंग",
+ "Go": "जाना"
+ }
+} \ No newline at end of file
diff --git a/lang/hu.json b/lang/hu.json
new file mode 100644
index 0000000..9e96475
--- /dev/null
+++ b/lang/hu.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Teszt"
+ }
+} \ No newline at end of file
diff --git a/lang/id.json b/lang/id.json
new file mode 100644
index 0000000..ae9fc6e
--- /dev/null
+++ b/lang/id.json
@@ -0,0 +1,15 @@
+{
+ "LoginLdap": {
+ "MenuLdap": "Pengguna LDAP",
+ "LoadUser": "Muat pengguna dari LDAP",
+ "Settings": "Pengaturan LDAP",
+ "AliasField": "Kolom Alias Pengguna",
+ "AdminPass": "Sandi lewat LDAP",
+ "LDAPServers": "Server LDAP",
+ "OneUser": "1 pengguna",
+ "Test": "Percobaan",
+ "ServerName": "Nama Server",
+ "FirstNameField": "Kolom Nama Depan",
+ "LastNameField": "Kolom Nama Belakang"
+ }
+} \ No newline at end of file
diff --git a/lang/it.json b/lang/it.json
new file mode 100644
index 0000000..ea3e01f
--- /dev/null
+++ b/lang/it.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Carica il plugin con l'autenticazione LDAP. Dà un'alternativa al plugin di accesso standard di Matomo.",
+ "MenuLdap": "Utenti LDAP",
+ "LoadUser": "Carica utente da LDAP",
+ "LoadUserDescription": "Puoi utilizzare questo form per sincronizzare in LDAP un singolo utente dalla UI.",
+ "LoadUserCommandDesc": "Per sincronizzare altri utenti, utilizza il comando %s.",
+ "Settings": "Impostazioni LDAP",
+ "AliasField": "Campo Alias Utente",
+ "BaseDn": "DN base",
+ "LdapPort": "Porta Server",
+ "MailField": "Campo Email",
+ "ServerUrl": "URL Server",
+ "UserIdField": "Campo ID Utente",
+ "AliasFieldDescription": "Attributo LDAP usato per generare un alias utente di Matomo, es. \"cn\".",
+ "MailFieldDescription": "Attributo LDAP che contiene l'indirizzo email di un utente, es. \"mail\".",
+ "UserIdFieldDescription": "Attributo LDAP che contiene il nome utente, es. \"uid\" o per Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "L'utente \"%s\" non è stato trovato!",
+ "NoUserName": "Nessun nome utente!",
+ "UsernameSuffix": "Suffisso Indirizzo Email",
+ "UsernameSuffixDescription": "Aggiunto agli user name per generare indirizzi email per quegli utenti che non ne hanno in LDAP. Imposta a, es. \"@localhost.com\".",
+ "AdminUser": "Nome utente LDAP Bind",
+ "AdminUserDescription": "Utente con accesso agli elementi di altri utenti, altrimenti lasciare vuoto per anonimo.",
+ "AdminPass": "Password LDAP",
+ "MemberOf": "Gruppo Utente Richiesto",
+ "MemberOfDescription": "Gruppo di cui l'utente è necessario sia membro, es. \"CN=Utente Matomo, CN=Utenti, DC=organizzazione, DC=com\".",
+ "MemberOfDescription2": "Nota: Questa impostazione utilizza il filtro LDAP memberOf=?. Alcuni server LDAP richiedono delle impostazioni extra per funzionare.",
+ "Filter": "Filtro di ricerca LDAP",
+ "FilterDescription": "Filtro LDAP utilizzato per determinare quali elementi possono essere utilizzati per l'autenticazione. Es. \"(objectClass=persona)\".",
+ "Kerberos": "Usa Web Server Auth (es. Kerberos SSO)",
+ "KerberosDescription": "Se abilitato, il plugin controllerà la variabile $_SERVER['REMOTE_USER'] e riterrà l'utente come già autenticato. Se $_SERVER['REMOTE_USER'] non fosse presente, tutti gli utenti verranno considerati autenticati, secondo le altre impostazioni.",
+ "LdapUserAdded": "Utente LDAP aggiunto con successo!",
+ "LdapFunctionsMissing": "L'estensione PHP LDAP sembra non essere abilitata. essendo richiesta da questo plugin, si prega di installarla.",
+ "CannotConnectToServer": "Impossibile connettersi al server LDAP.",
+ "CannotConnectToServers": "Impossibile connettersi a nessuno dei %s server LDAP.",
+ "LDAPServers": "Server LDAP",
+ "OneUser": "1 utente",
+ "MemberOfCount": "%s sono membri di questo gruppo.",
+ "FilterCount": "%s hanno una corrispondenza con questo filtro.",
+ "Test": "Prova",
+ "InvalidFilter": "Filtro non valido.",
+ "ServerName": "Nome del server",
+ "FirstNameField": "Campo Nome",
+ "FirstNameFieldDescription": "Attributo LDAP che contiene il nome dell'utente.",
+ "LastNameField": "Campo Cognome",
+ "LastNameFieldDescription": "Attributo LDAP che contiene il cognome di un utente.",
+ "FirstLastNameForAlias": "Nome e cognome sono utilizzati per generare un alias se non ne viene trovato uno in LDAP.",
+ "NewUserDefaultSitesViewAccess": "Siti iniziali con un accesso in lettura per i nuovi utenti",
+ "NewUserDefaultSitesViewAccessDescription": "Se specificato, quando un utente LDAP è sincronizzato per la prima volta, questi avrà l'accesso in lettura a questi siti. Deve essere impostato su una lista di ID o su 'tutti'.",
+ "EnableLdapAccessSynchronization": "Abilita Sincronizzazione d'Accesso Utente LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Se abilitato, i livelli d'accesso utente sono determinati da attributi personalizzati LDAP. Per utilizzare questa funzionalità dovrai modificare il tuo scherma del server LDAP.",
+ "LdapViewAccessField": "Campo LDAP solo visione",
+ "LdapViewAccessFieldDescription": "Attributo personalizzato LDAP che determina a quali siti un utente ha l'accesso in lettura.",
+ "LdapAdminAccessField": "Campo Accesso Amministratore LDAP",
+ "LdapAdminAccessFieldDescription": "Attributo personale LDAP che determina a quali siti un utente ha l'accesso da amministratore.",
+ "LdapSuperUserAccessField": "Campo Accesso Super User LDAP",
+ "LdapSuperUserAccessFieldDescription": "Attributo LDAP personalizzato che determina se un utente è o meno un superuser.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Delimitatore Specifiche Server Attributi Accesso Utente",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Stringa utilizzata per delimitare le specifiche del server in un attributo di accesso utente. Se impostata a ';', ci si aspetta che l'attributo di accesso sia simile a '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Server Attributi Accesso Utente & Separatore Elenco Siti",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Stringa usata per separare gli ID delle istanze server dagli ID elenchi siti. Se impostata a ':', ci si aspetterà un attributo di accesso simile a '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Nome speciale per questa istanza di Matomo",
+ "ThisMatomoInstanceNameDescription": "Nome speciale utilizzato per identificare questa istanza di Matomo in un attributo di accesso. Se non è specificato, ci si aspetta l'URL base di questo Matomo.",
+ "UnsupportedPasswordReset": "Impossibile reimpostare la password di questo utente, si prega di contattare un amministratore.",
+ "UserSyncSettings": "Impostazioni di sincronizzazione utente",
+ "AccessSyncSettings": "Impostazioni Sincronizzazione d'Accesso",
+ "SynchronizeUsersAfterLogin": "Sincronizza gli utenti dopo il login avvenuto con successo",
+ "SynchronizeUsersAfterLoginDescription": "Se abilitato, i dettagli utente in LDAP saranno sempre sincronizzati dopo un login avvenuto con successo.",
+ "SynchronizeUsersAfterLoginDescription2": "Se disabilitato, il plugin non sincronizzerà le informazioni sull'utente dopo l'accesso, a meno che l'autenticazione LDAP non sia abilitata. Quando possibile, le connessioni LDAP verranno completamente evitate.",
+ "UseLdapForAuthentication": "Usa sempre LDAP per l'autenticazione",
+ "UseLdapForAuthenticationDescription": "Se disabilitato, le password utente verranno immagazzinate nel DB di Matomo oltre che in LDAP. Questo favorirà accessi più rapidi, ma è meno sicuro.",
+ "PasswordField": "Campo Password utente",
+ "PasswordFieldDescription": "Nome dell'attributo LDAP che contiene la password di un utente, es. 'userPassword' o 'unicodePwd'.",
+ "PasswordFieldDescription2": "Se è abilitato 'Usa sempre LDAP per l'autenticazione', ed è disabilitato 'Genera un token auth casuale per i nuovi visitatori', il valore di questo campo in LDAP deve essere una password utente hashed o criptata.",
+ "ReadMoreAboutAccessSynchronization": "Per saperne di più riguardo alla sincronizzazione dell'accesso utente, %1$sleggi la documentazione%2$s.",
+ "ExpectedLdapAttributes": "Attributi LDAP attesi",
+ "ExpectedLdapAttributesPrelude": "Con questa configurazione, LoginLdap si aspetta degli attributi in LDAP che somigliano a",
+ "NetworkTimeout": "Timeout Richiesta Rete LDAP (in sec.)",
+ "NetworkTimeoutDescription": "Limite di tempo in secondi per la richiesta per una rete LDAP.",
+ "NetworkTimeoutDescription2": "Maggiore è il timeout, più gli utenti devono attendere prima che Matomo li possa far accedere tramite LDAP, se uno o più server non rispondono.",
+ "Go": "Vai",
+ "LoginPluginEnabledWarning": "Sono abilitati entrambi i plugin %1$s e %2$s! Questo impedirà l'accesso agli utenti LDAP, si prega di disabilitare il plugin %1$s.",
+ "MemberOfField": "Campo LDAP memberOf",
+ "MemberOfFieldDescription": "Campo usato dal tuo LDAP per indicare il tipo di utenza, di default \"memberOf\"",
+ "LdapUrlPortWarning": "L'opzione porta viene ignorata quando l'hostname del server è impostato come URL LDAP (cioè, ldap:\/\/localhost\/ invece di localhost).",
+ "UpdateFromPre300Warning": "Dato che hai aggiornato da una versione precedente alla 3.0.0, probabilmente dovrai mantenere non spuntata questa opzione. Prima della versione 3.0.0, password e nome utente erano entrambi immagazzinati sia in LDAP che nel DB di Matomo. Questa opzione non deve essere spuntata, in modo che anche gli utenti registrati prima dell'aggiornamento possano accedere.",
+ "MobileAppIntegrationNote": "Nota: se prevedi di utilizzare LDAP con l'app mobile ufficiale, devi mantenere non spuntata questa opzione. Al momento, l'app mobile non può autenticare gli utenti se nome utente e password non sono immagazzinati nel DB di Matomo.",
+ "PasswordFieldHelp": "Inserisci una password per sovrascrivere quella esistente o lascia vuoto per non fare cambiamenti.",
+ "LdapUserCantChangePassword": "Il cambio password non è supportato dagli utenti LDAP. Se utilizzi LDAP, le tue impostazioni utente vengono gestite direttamente da LDAP. Per ulteriori informazioni, si prega di contattare il tuo amministratore del server LDAP o il tuo amministratore di Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/ja.json b/lang/ja.json
new file mode 100644
index 0000000..63df769
--- /dev/null
+++ b/lang/ja.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "LDAP 認証でプラグインにログインしてください。標準 Matomo ログイン プラグインの代替手段を提供します。",
+ "MenuLdap": "LDAP ユーザー",
+ "LoadUser": "LDAP からのロード ユーザー",
+ "LoadUserDescription": "このフォームを使用すると、UI から LDAP の 1 人のユーザーを同期することができます。",
+ "LoadUserCommandDesc": "多くのユーザーを同期するには、%s コマンドを使用します。",
+ "Settings": "LDAP 設定",
+ "AliasField": "エイリアスフィールド",
+ "BaseDn": "ベース DN",
+ "LdapPort": "サーバーポート",
+ "MailField": "メールフィールド",
+ "ServerUrl": "サーバー URL",
+ "UserIdField": "ユーザー ID フィールド",
+ "AliasFieldDescription": "ユーザの Matomo エイリアスを生成するために使用される LDAP 属性。 \"cn\"",
+ "MailFieldDescription": "ユーザーの電子メールアドレスを含む LDAP 属性。 \"mail\"",
+ "UserIdFieldDescription": "ユーザーのユーザー名を含む LDAP 属性(例: \"uid\"、または Active Directory の場合: \"userPrincipalName\"",
+ "UserNotFound": "ユーザー \"%s\" が見つかりません!",
+ "NoUserName": "ユーザーネームがありません!",
+ "UsernameSuffix": "ユーザーネーム拡張子 \/ ドメイン",
+ "UsernameSuffixDescription": "ユーザー名に追加され、LDAP にないユーザーの電子メールアドレスが生成されます。 \"@ localhost.com\" などに設定します。",
+ "AdminUser": "LDAP に結びついたユーザー名",
+ "AdminUserDescription": "( 例 \"john\" は、他ユーザーを照会する読み取りアクセス権を持っている必要があります。)",
+ "AdminPass": "LDAP パスワード",
+ "MemberOf": "アクティブディレクトリーグループ",
+ "MemberOfDescription": "( 例 \"CN=Matomo User,CN=Users,DC=organization,DC=com\" )",
+ "MemberOfDescription2": "注:この設定は、memberOf=? LDAP フィルタを使用しています。一部の LDAP サーバーでは、これが機能するための追加の設定が必要です。",
+ "Filter": "LDAP 検索フィルター",
+ "FilterDescription": "どのエンティティが認証に使用できるかを判断するために使用される LDAP フィルタ。 \"(objectClass = person)\"",
+ "Kerberos": "Webサーバー認証 ( Kerberos SSOなど ) を使用する",
+ "KerberosDescription": "有効にすると、プラグインは $ _SERVER ['REMOTE_USER'] 変数をチェックし、ユーザーが既に認証されていると想定します。 $ _SERVER ['REMOTE_USER'] が存在しない場合は、他の設定に従ってすべてのユーザーが認証されます。",
+ "LdapUserAdded": "LDAP ユーザーの追加に成功しました!",
+ "LdapFunctionsMissing": "PHP の ldap_connect() ファンクションが見当たりません。PHP コンフィグで LDAP を有効にしてください !",
+ "CannotConnectToServer": "LDAP サーバーに接続できません。",
+ "CannotConnectToServers": "%s LDAP サーバーのいずれかに接続できません。",
+ "LDAPServers": "LDAP サーバー",
+ "OneUser": "1 ユーザー",
+ "MemberOfCount": "%s はこのグループのメンバーです。",
+ "FilterCount": "%s は、このフィルターに一致しています。",
+ "Test": "テスト",
+ "InvalidFilter": "無効なフィルター。",
+ "ServerName": "サーバーネーム",
+ "FirstNameField": "ファーストネームフィールド",
+ "FirstNameFieldDescription": "ユーザーのファーストネームを含む LDAP 属性。",
+ "LastNameField": "ラストネームフィールド",
+ "LastNameFieldDescription": "ユーザーの姓を含む LDAP 属性。",
+ "FirstLastNameForAlias": "LDAP に見つからない場合はファースト及びラストネームを使用して、エイリアスを生成します。",
+ "NewUserDefaultSitesViewAccess": "最初のサイトは新規ユーザーに対して閲覧権限を与えます",
+ "NewUserDefaultSitesViewAccessDescription": "指定された場合は、LDAP ユーザーが最初に同期された時、彼 ( 女 ) はこれらのサイトへの閲覧権限が付与されます。 ID のリストまたは「すべて」に設定する必要があります。",
+ "EnableLdapAccessSynchronization": "LDAP からのユーザー アクセスの同期を有効にします。",
+ "EnableLdapAccessSynchronizationDescription": "有効にした場合、ユーザーのアクセス レベルは、カスタム LDAP 属性によって決定されます。注: この機能を使用するには、LDAP サーバーのスキーマを変更する必要があります。",
+ "LdapViewAccessField": "LDAP ビューアクセスフィールド",
+ "LdapViewAccessFieldDescription": "ユーザがどのサイトの閲覧権限を持っているかを決定するカスタム LDAP 属性。",
+ "LdapAdminAccessField": "LDAP 管理者アクセス フィールド",
+ "LdapAdminAccessFieldDescription": "ユーザーが管理アクセス権を持つサイトを決定するカスタム LDAP 属性。",
+ "LdapSuperUserAccessField": "LDAP スーパーユーザーアクセスフィールド",
+ "LdapSuperUserAccessFieldDescription": "ユーザがスーパーユーザであるかどうかを決定するカスタム LDAP 属性です。",
+ "LdapUserAccessAttributeServerSpecDelimiter": "ユーザーアクセス属性サーバー指定の区切り文字",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "ユーザーのアクセス属性でサーバーの仕様を区切るために使用される文字列。 ' ; ' に設定すると、アクセス属性は '<server-spec>; <server-spec>; ...'のようになります。",
+ "LdapUserAccessAttributeServerSeparator": "ユーザーアクセス属性サーバーとサイトリストセパレータ",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Matomo サーバーインスタンス ID をサイト ID リストで区切るために使用される文字列。 ':' に設定すると、アクセス属性は '<server-id>:<site-list>; <server-id>:<site-list>'のようになります。",
+ "ThisMatomoInstanceName": "この Matomo インスタンスの特別な名前",
+ "ThisMatomoInstanceNameDescription": "アクセス属性でこの Matomo インスタンスを識別するために使用される特別な名前。 指定されていない場合、この Matomo のベース URL が必要です。",
+ "UnsupportedPasswordReset": "このユーザーのパスワードはリセットできません。管理者にお問い合わせください。",
+ "UserSyncSettings": "ユーザーの同期設定",
+ "AccessSyncSettings": "アクセス同期設定",
+ "SynchronizeUsersAfterLogin": "ログイン成功後にユーザーを同期します",
+ "SynchronizeUsersAfterLoginDescription": "有効な場合、LDAP のユーザーの詳細はログインに成功後、常に同期されます。",
+ "SynchronizeUsersAfterLoginDescription2": "無効にした場合、プラグインはログイン後、LDAP 認証が有効になっていないユーザー情報が同期されません。可能な場合は LDAP 接続を完全に回避します。",
+ "UseLdapForAuthentication": "認証には常に LDAP を使用します",
+ "UseLdapForAuthenticationDescription": "無効の場合、ユーザーパスワードは LDAP に加えて Matomo の DB に蓄えられます。これによりログインは速くなりますが、安全性は低下します。",
+ "PasswordField": "ユーザー パスワード フィールド",
+ "PasswordFieldDescription": "ユーザーのパスワード、'userPassword' または 'unicodePwd' などを含む LDAP 属性の名前。",
+ "PasswordFieldDescription2": "'常に認証に LDAP を使用してください' を有効にすると '新規ユーザーのためのランダムtoken_auth を生成' は無効となり、LDAP のこのフィールドの値はユーザーのハッシュまたは暗号化されたパスワードにする必要があります。",
+ "ReadMoreAboutAccessSynchronization": "ユーザー アクセスの同期についての詳細は、%1$s ドキュメントをお読みください %2$s 。",
+ "ExpectedLdapAttributes": "期待される LDAP 属性",
+ "ExpectedLdapAttributesPrelude": "この構成により、LoginLdap は次のようになり、LDAP 内の属性を期待します",
+ "NetworkTimeout": "LDAP ネットワークリクエストタイムアウト (秒)",
+ "NetworkTimeoutDescription": "LDAP ネットワークリクエストの秒単位のタイムリミット。",
+ "NetworkTimeoutDescription2": "1つ以上のサーバーに反応がない時、タイムアウトの時間が長ければ長いほど、Matomo が LDAP を通してログインするまで、ユーザーはより長い時間待機しなければなりません。",
+ "Go": "Go",
+ "LoginPluginEnabledWarning": "%1$s と %2$s のプラグインがどちらも有効になっています! LDAP ユーザのログインが失敗する原因になりますので、%1$s のプラグインを無効にしてください。",
+ "MemberOfField": "LDAP memberOf フィールド",
+ "MemberOfFieldDescription": "デフォルトの \"memberOf\" というメンバーシップを示すために LDAP により使われたフィールド",
+ "LdapUrlPortWarning": "サーバのホスト名は LDAP URL (すなわち、localhost の代わりに ldap:\/\/localhost\/) に設定されている場合、ポートオプションは無視されます。",
+ "UpdateFromPre300Warning": "3.0.0 以前のバージョンから更新した場合は、このオプションをチェックしないでください。 バージョン 3.0.0 以前では、ユーザーパスワードは LDAP と Matomo DB の両方に保存されていました。 アップグレード前に追加したユーザーがログインできるようにするには、このオプションをオフにする必要があります。",
+ "MobileAppIntegrationNote": "注: 公式モバイル アプリで LDAP を使用している場合は、このオプションはオフにしておく必要があります。現在、携帯アプリでは、ユーザーのパスワードは Matomo の DB に格納されていない場合ユーザーを認証できません。",
+ "PasswordFieldHelp": "既存のパスワードに上書きするか、ブランクのままにして変更しないでください。",
+ "LdapUserCantChangePassword": "LDAP ユーザーの場合、パスワードの変更はサポートされていません。 LDAP を使用する場合、ユーザー設定は LDAP で直接管理されます。詳細については、LDAP サーバー管理者または Matomo 管理者にお問い合わせください。"
+ }
+} \ No newline at end of file
diff --git a/lang/ka.json b/lang/ka.json
new file mode 100644
index 0000000..70401cc
--- /dev/null
+++ b/lang/ka.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "ტესტი"
+ }
+} \ No newline at end of file
diff --git a/lang/ko.json b/lang/ko.json
new file mode 100644
index 0000000..fd981c5
--- /dev/null
+++ b/lang/ko.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "테스트"
+ }
+} \ No newline at end of file
diff --git a/lang/lt.json b/lang/lt.json
new file mode 100644
index 0000000..acf742b
--- /dev/null
+++ b/lang/lt.json
@@ -0,0 +1,11 @@
+{
+ "LoginLdap": {
+ "Settings": "LDAP nustatymai",
+ "LdapPort": "Serverio prievadas",
+ "UserNotFound": "Naudotojas \"%s\" nerastas!",
+ "AdminPass": "LDAP slaptažodis",
+ "LDAPServers": "LDAP serveriai",
+ "Test": "Testas",
+ "InvalidFilter": "Neteisingas filtras."
+ }
+} \ No newline at end of file
diff --git a/lang/lv.json b/lang/lv.json
new file mode 100644
index 0000000..0d324f7
--- /dev/null
+++ b/lang/lv.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Testēt"
+ }
+} \ No newline at end of file
diff --git a/lang/nb.json b/lang/nb.json
new file mode 100644
index 0000000..fbf52f9
--- /dev/null
+++ b/lang/nb.json
@@ -0,0 +1,18 @@
+{
+ "LoginLdap": {
+ "MenuLdap": "LDAP-brukere",
+ "LoadUser": "Hent bruker fra LDAP",
+ "LoadUserCommandDesc": "For å synkronisere flere brukere, bruk %s kommandoen.",
+ "Settings": "LDAP-innstillinger",
+ "UserNotFound": "Brukeren «%s» ble ikke funnet!",
+ "AdminPass": "LDAP-passord",
+ "Filter": "LDAP-søkefilter",
+ "LDAPServers": "LDAP-servere",
+ "OneUser": "1 bruker",
+ "MemberOfCount": "%s er medlem av denne gruppen.",
+ "Test": "Test",
+ "InvalidFilter": "Ugyldig filter.",
+ "ServerName": "Servernavn",
+ "Go": "Gå"
+ }
+} \ No newline at end of file
diff --git a/lang/nl.json b/lang/nl.json
new file mode 100644
index 0000000..aaadc8c
--- /dev/null
+++ b/lang/nl.json
@@ -0,0 +1,76 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Login plugin met LDAP authenticatie. Alternatief voor de standaard Matomo Login plugin.",
+ "MenuLdap": "LDAP Gebruikers",
+ "LoadUser": "Laad Gebruiker uit LDAP",
+ "LoadUserDescription": "Je kunt dit formulier gebruiken om een enkele gebruiker in LDAP te synchroniseren vanuit de UI.",
+ "LoadUserCommandDesc": "Om meer gebruikers te synchroniseren, gebruik het %s commando.",
+ "Settings": "LDAP instellingen",
+ "AliasField": "Gebruiker Bijnaam Veld",
+ "BaseDn": "Basis DN",
+ "LdapPort": "Serverpoort",
+ "MailField": "E-mailadres-veld",
+ "ServerUrl": "Server-URL",
+ "UserIdField": "Gebruiker-ID-veld",
+ "AliasFieldDescription": "te gebruiken LDAP attribuut om een Matomo gebruiker alias aan te maken, bijv. \"cn\".",
+ "MailFieldDescription": "Het LDAP attribuut die de gebruikers emailadres bevat, bijv. \"mail\".",
+ "UserIdFieldDescription": "Het LDAP attribuut die de gebruikers gebruikersnaam bevat, bijv. \"uid\", of in Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "Gebruiker \"%s\" niet gevonden!",
+ "NoUserName": "Geen gebruikersnaam opgegeven!",
+ "UsernameSuffix": "E-mail adres achtervoegsel",
+ "UsernameSuffixDescription": "toegevoegd aan de gebruikersnaam om een email adres aan te maken voor gebruikers dat er geen hebben in LDAP. Bijv: \"@localhost.com\"",
+ "AdminUser": "LDAP Bind Gebruikersnaam",
+ "AdminUserDescription": "Gebruikers met toegang tot andere gebruikersinfo. Laat leeg voor anonieme koppelingen.",
+ "AdminPass": "LDAP Wachtwoord",
+ "MemberOf": "Vereiste gebruikersgroep",
+ "MemberOfDescription2": "Let op\"deze instelling gebruikt het memberOf=? LDAP filter. Sommige LDAP servers hebben extra instellingen nodig om dit te laten werken.",
+ "Filter": "LDAP Zoek Filter",
+ "FilterDescription": "Een LDAP filter wordt gebruikt om te bepalen welke entiteiten mogen worden gebruikt voor verificatie , bijv \" ( objectClass = persoon) \" .",
+ "Kerberos": "Gebruik Web server authenticatie (bijv. Kerberos SSO)",
+ "LdapUserAdded": "LDAP-gebruiker succesvol toegevoegd!",
+ "LdapFunctionsMissing": "Het lijkt erop dat de PHP LDAP extensie niet is ingeschakeld. Deze is vereist voor de plugin. Gelieve deze te installeren.",
+ "CannotConnectToServer": "Kan niet verbinden met de LDAP server.",
+ "CannotConnectToServers": "Kan geen verbinding maken met elk van de %s LDAP servers.",
+ "LDAPServers": "LDAP Servers",
+ "OneUser": "1 gebruiker",
+ "MemberOfCount": "%s is lid van deze groep.",
+ "FilterCount": "%s komen overeen met deze filter.",
+ "Test": "Testen",
+ "InvalidFilter": "Ongeldige filter.",
+ "ServerName": "Servernaam",
+ "FirstNameField": "Voornaam-veld",
+ "FirstNameFieldDescription": "Het LDAP attribuut dat de gebruikers voornaam bevat.",
+ "LastNameField": "Achternaam-veld",
+ "LastNameFieldDescription": "Het LDAP attribuut ddie de gebruikers achternaam bevat.",
+ "FirstLastNameForAlias": "De voornaam en achternaam worden gebruikt om een alias aan te maken wanneer er geen gevonden kan worden in LDAP.",
+ "NewUserDefaultSitesViewAccess": "Standaard websites met lees rechten voor nieuwe gebruikers",
+ "EnableLdapAccessSynchronization": "Schakel Gebruikers Toegang Synchronisatie in vanaf LDAP",
+ "LdapViewAccessField": "LDAP View Access Veld",
+ "LdapViewAccessFieldDescription": "Het aangepaste LDAP attribuut dat bepaalt welke sites een gebruiker mag bekijken.",
+ "LdapAdminAccessField": "LDAP Admin Access veld.",
+ "LdapAdminAccessFieldDescription": "Het standaard LDAP attribuut dat bepaalt tot welke websites een gebruiker admin rechten geeft.",
+ "LdapSuperUserAccessField": "LDAP Super Gebruiker Toegang Veld",
+ "LdapSuperUserAccessFieldDescription": "Het standaard LDAP attribuut dat bepaalt of een gebruiker een superuser is of niet.",
+ "ThisMatomoInstanceName": "Speciale naam voor deze Matomo instantie",
+ "UnsupportedPasswordReset": "Kan wachtwoord van deze gebruiker niet resetten, neem a.u.b. contact op met een administrator.",
+ "UserSyncSettings": "Gebruiker Synchronisatie Instellingen",
+ "AccessSyncSettings": "Toegang synchronisatie instellingen",
+ "SynchronizeUsersAfterLogin": "Synchroniseer gebruikers na een succesvolle login.",
+ "SynchronizeUsersAfterLoginDescription": "Als ingesteld, gebruiker details in LDAP worden altijd gesynchroniseerd na een succesvolle aanmelding.",
+ "UseLdapForAuthentication": "Altijd LDAP gebruiken voor authenticatie",
+ "UseLdapForAuthenticationDescription": "Indien uitgeschakeld, worden gebruikerswachtwoorden in de database van Matomo opgeslagen in plaats van LDAP. Dit resulteert in sneller inloggen, maar is minder veilig.",
+ "PasswordField": "Gebruiker Wachtwoord Veld",
+ "ReadMoreAboutAccessSynchronization": "Om meer te weten over gebruikers toegang synchronisatie, %1$slees onze documentatie%2$s.",
+ "ExpectedLdapAttributes": "Verwachte LDAP attributen",
+ "ExpectedLdapAttributesPrelude": "Met deze configuratie, LoginLdap verwacht attributen in LDAP die eruit zien als",
+ "NetworkTimeout": "LDAP Netwerk verzoek Timeout (in seconden)",
+ "NetworkTimeoutDescription": "De tijdslimiet in seconden voor een LDAP netwerk verzoek.",
+ "Go": "Ga",
+ "LoginPluginEnabledWarning": "Beide %1$s en%2$s plugins zijn geactiveerd! Dit zorgt ervoor dat login pogingen van LDAP gebruikers niet slagen, schakel de plugin %1$s uit.",
+ "MemberOfField": "LDAP memberOf Veld",
+ "MemberOfFieldDescription": "Veld in je LDAP installatie om lidmaatschap aan te geven, standaard \"memberOf\"",
+ "LdapUrlPortWarning": "De port optie wordt genegeerd wanneer de hostname van de server wordt aangepast naar een LDAP URL (bijvoorbeeld, ldap:\/\/localhost\/ in plaats van localhost).",
+ "MobileAppIntegrationNote": "Opmerking: als u van plan bent om LDAP met de officiële mobiele app te gebruiken, moet u deze optie niet aanvinken. Momenteel kan de mobiele app gebruikers niet authenticeren als gebruikerswachtwoorden niet in de database van Matomo zijn opgeslagen.",
+ "PasswordFieldHelp": "Voer een wachtwoord in om het bestaande wachtwoord te overschrijven of laat deze leeg, dan blijft het huidige bestaan."
+ }
+} \ No newline at end of file
diff --git a/lang/nn.json b/lang/nn.json
new file mode 100644
index 0000000..3907deb
--- /dev/null
+++ b/lang/nn.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Test"
+ }
+} \ No newline at end of file
diff --git a/lang/pl.json b/lang/pl.json
new file mode 100644
index 0000000..0b0ffc8
--- /dev/null
+++ b/lang/pl.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Wtyczka logowania wykorzystująca autentykację LDAP. Dostarcza alternatywną do standardowej metody logowania Matomo'a.",
+ "MenuLdap": "Użytkownicy LDAP",
+ "LoadUser": "Załaduj użytkowników z LDAP'a",
+ "LoadUserDescription": "Ten formularz pozwala na synchronizację pojedynczego użytkownika w LDAP'ie przy pomocy interfejsu.",
+ "LoadUserCommandDesc": "W celu synchronizacji większej liczby użytkowników, użyj polecenia %s.",
+ "Settings": "Ustawienia LDAP",
+ "AliasField": "Pole aliasu użytkownika",
+ "BaseDn": "Podstawowy DN",
+ "LdapPort": "Port Serwera",
+ "MailField": "Pole adresu e-mail",
+ "ServerUrl": "URL serwera",
+ "UserIdField": "Pole ID użytkownika",
+ "AliasFieldDescription": "Atrybut LDAP używany do wygenerowania Matomo'owego aliasu użytkownika, np.: \"cn\".",
+ "MailFieldDescription": "Atrybut LDAP zawierający adres e-mail użytkownika, np. \"e-mail\"",
+ "UserIdFieldDescription": "Atrybut LDAP zawierający nazwę użytkownika, np.: \"uid\" lub w Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "Nie znaleziono użytkownika \"%s\"!",
+ "NoUserName": "Brak nazwy użytkownika!",
+ "UsernameSuffix": "Sufiks adresu e-mail",
+ "UsernameSuffixDescription": "Dołączane do nazwy użytkownika w celu wygenerowania adresu e-mail dla użytkowników nie posiadających konta w LDAP'ie. Ustaw na przykład na \"@localhost.com\";",
+ "AdminUser": "Powiązana nazwa użytkownika LDAP",
+ "AdminUserDescription": "Użytkownik z dostępem do wpisów innych użytkowników, lub puste dla anonimowych powiązań.",
+ "AdminPass": "Hasło LDAP",
+ "MemberOf": "Wymagana grupa użytkowników",
+ "MemberOfDescription": "Grupa, której członkiem musi być użytkownik, np.: \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "NOTKA: To ustawnienie wykorzystuje filtr LDAP memberOf=?. Niektóre serwery LDAP wymagają dodatkowej konfiguracji, aby ten filtr zadziałał.",
+ "Filter": "Filtr wyszukiwania LDAP",
+ "FilterDescription": "Filtr LDAP wykorzystywany w celu ustalenia, które encje mogą zostać wykorzystane do autentykacji, np.: \"(objectClass=person)\".",
+ "Kerberos": "Użyj Autentykacji Serwera WWW (n.p. Kerberos SSO)",
+ "KerberosDescription": "Po włączeniu, wtyczka będzie sprawdzała zmienną $_SERVER['REMOTE_USER'] zakładając, że użytkownik został autoryzowany. W przypadku braku zmiennej $_SERVER['REMOTE_USER'], wszyscy użytkownicy są autentykowani na podstawie innych ustawień.",
+ "LdapUserAdded": "Udało się dodać użytkownika LDAP!",
+ "LdapFunctionsMissing": "Rozszerzenie PHP LDAP wydaje się nie być włączone. Jest ono wymagane przez tą wtyczkę, więc proszę zainstaluj je.",
+ "CannotConnectToServer": "Brak połączenia z serwerem LDAP.",
+ "CannotConnectToServers": "Nie udało się połączyć z żadnym z %s serwerów LDAP.",
+ "LDAPServers": "Serwery LDAP",
+ "OneUser": "1 użytkownik",
+ "MemberOfCount": "%s jest członkami tej grupy.",
+ "FilterCount": "%s zostało dopasowanych do tego filtra.",
+ "Test": "Test",
+ "InvalidFilter": "Niewłaściwy filtr.",
+ "ServerName": "Nazwa serwera",
+ "FirstNameField": "Pole Imię",
+ "FirstNameFieldDescription": "Atrybut LDAP zawierający imię użytkownika.",
+ "LastNameField": "Pole Nazwisko",
+ "LastNameFieldDescription": "Atrybut LDAP zawierający nazwisko użytkownika.",
+ "FirstLastNameForAlias": "Imię i nazwisko są wykorzystywane do tworzenia aliasu jeśli nie uda się go znaleźć w LDAP'ie.",
+ "NewUserDefaultSitesViewAccess": "Strony z podglądem dostępne dla Nowych Użytkowników",
+ "NewUserDefaultSitesViewAccessDescription": "Jeśli zaznaczone, w momencie pierwszej synchronizacji nowego użytkownika z LDAP, użytkownik otrzymuje dostęp do tych serwisów. Powinno być listą ID lub 'all'.",
+ "EnableLdapAccessSynchronization": "Włącz synchronizację dostępu użytkowników z LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Włączenie pozwala określać dostęp użytkowników do Matomo'a poprzez dodatkowe atrybuty LDAP. NOTKA: W celu użycia funkcjonalności, konieczna będzie modyfikacja schematu Twojego serwera LDAP.",
+ "LdapViewAccessField": "LDAP - Pole uprawnień do podglądu",
+ "LdapViewAccessFieldDescription": "Konfigurowalny atrybut LDAP określający, w których serwisach użytkownik ma dostęp do podglądu.",
+ "LdapAdminAccessField": "LDAP - Pole uprawnień administratorskich",
+ "LdapAdminAccessFieldDescription": "Konfigurowalny atrybut LDAP określający, w których serwisach użytkownikowi nadano uprawnienia administratorskie.",
+ "LdapSuperUserAccessField": "LDAP - Pole uprawnień Super Użytkownika",
+ "LdapSuperUserAccessFieldDescription": "Konfigurowalny atrybut LDAP określający serwisy, w których użytkownikowi nadano uprawnienia Super Użytkownika.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Separator konfiguracji serwera dla atrybutu dostępu użytkownika",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Ciąg skonfigurowany na serwerze, wykorzystywany do separacji atrybutów dostępu użytkownika. Ustawienie ';' spowoduje, że atrybut dostępu będzie zwracany jako '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Serwerowy atrybut dostępu użytkownika i separator listy serwisów",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Ciąg wykorzystywany do oddzielenia identyfikatorów serwerów Matomo z identyfikatorami serwisów. Ustawienie ':' wygeneruje taki atrybut '<server-id>:<site-list>:<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Specjalna nazwa tej instancji Matomo'a",
+ "ThisMatomoInstanceNameDescription": "Specjalna nazwa pozwala na identyfikację tej instancji Matomo'a w atrybucie dostępu. Domyślną wartością będzie bazowy adres tego Matomo'a",
+ "UnsupportedPasswordReset": "Nie udało się zresetować hasła tego użytkownika, proszę skontaktuj się z administratorem.",
+ "UserSyncSettings": "Ustawienia Synchronizacji Użytkowników",
+ "AccessSyncSettings": "Ustawienia Synchronizacji Dostępów",
+ "SynchronizeUsersAfterLogin": "Synchronizuj Użytkowników po poprawnym logowaniu",
+ "SynchronizeUsersAfterLoginDescription": "Włączenie opcji uruchomi synchronizację danych Użytkownika z LDAP'a po każdorazowym poprawnym logowaniu.",
+ "SynchronizeUsersAfterLoginDescription2": "Wyłączenie wtyczki zapobiega synchronizacji użytkownika po zalogowaniu, chyba że autoryzacja LDAP jest włączona. Zawsze, gdy to możliwe, aplikacja będzie unikała nawiązywania połączeń LDAP.",
+ "UseLdapForAuthentication": "Zawsze używaj LDAP do autentykacji",
+ "UseLdapForAuthenticationDescription": "Wyłączenie spowoduje przechowywanie haseł użytkowników w bazie Matomo'a równolegle do LDAP. Przyspiesza to logowanie, ale jest mniej bezpieczne.",
+ "PasswordField": "Pole hasła Użytkownika",
+ "PasswordFieldDescription": "Nazwa atrybutu LDAP zawierającego hasło użytkownika, np. 'userPassword' lub 'unicodePwd'",
+ "PasswordFieldDescription2": "Po zaznaczeniu 'Zawsze używaj LDAP do autentykacji' i wyłączeniu 'Generuj Losowy token_auth Dla Nowych Użytkowników', wartością tego pola w LDAP'ie musi być zaszyfrowane hasło.",
+ "ReadMoreAboutAccessSynchronization": "Aby dowiedzieć się więcej o synchronizacji dostępów użytkowników, %1$sprzeczytaj dokumentację%2$s.",
+ "ExpectedLdapAttributes": "Oczekiwane atrybuty LDAP",
+ "ExpectedLdapAttributesPrelude": "Z taką konfiguracją wtyczka LoginLdap oczekuje, że atrybuty w LDAP'ie zapisane będą w postaci",
+ "NetworkTimeout": "Czas oczekiwania żądania sieciowego LDAP (w sek.)",
+ "NetworkTimeoutDescription": "Limit czasu na odpowiedź serwera LDAP w sekundach.",
+ "NetworkTimeoutDescription2": "Ustawienie dłuższego czasu oczekiwania na odpowiedź serwera, może spowodować wydłużenie czasu potrzebnego na logowanie w Matomo'u poprzez LDAP, w sytuacji gdy jeden lub więcej Twoich serwerów staną się niedostępne.",
+ "Go": "Idź",
+ "LoginPluginEnabledWarning": "Obie wtyczki %1$s i %2$s są włączone! To spowoduje, że nie uda się zalogować użytkowników poprzez LDAP, proszę wyłącz wtyczkę %1$s.",
+ "MemberOfField": "Pole LDAP memberOf",
+ "MemberOfFieldDescription": "Pole w Twoim LDAP'ie służące do wskazywania członkostwa, domyślnie \"memberOf\"",
+ "LdapUrlPortWarning": "Opcjonalny numer portu jest ignorowany jeśli nazwa hosta jest skonfigurowana jako LDAP URL (np., ldap:\/\/localhost\/ zamiast localhost).",
+ "UpdateFromPre300Warning": "Aplikacja została zaktualizowana z wersji starszej niż 3.0.0, pozostawienie tej opcji niezaznaczonej najprawdopodobniej będzie dobrym pomysłem. Przed wersją 3.0.0, hasła użytkowników zapisywane były w LDAP'ie i w bazie Matomo'a. Opcja ta musi pozostać niezaznaczona, tak aby użytkownicy dodani przed aktualizacją w dalszym ciągu byli w stanie się zalogować.",
+ "MobileAppIntegrationNote": "NOTKA: Jeśli zamierzasz korzystać z LDAP'a w oficjalnej aplikacji mobilnej, musisz pozostawić tą opcję niezaznaczoną. Obecnie, aplikacja mobilna nie potrafi autentykować użytkowników jeśli ich hasła nie są zapisane w bazie Matomo'a.",
+ "PasswordFieldHelp": "Wprowadź nowe hasło, aby nadpisać istniejące hasło lub pozostaw puste aby zaniechać zmian.",
+ "LdapUserCantChangePassword": "Zmiana hasła nie jest wspierana dla użytkowników autentykowanych przez LDAP. Korzystasz z LDAP, a więc Twoje ustawienia są zarządzane bezpośrednio w LDAP'ie. Więcej informacji na temat serwera LDAP uzyskasz u swojego administratora lub administratora Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/pt-br.json b/lang/pt-br.json
new file mode 100644
index 0000000..f2422f4
--- /dev/null
+++ b/lang/pt-br.json
@@ -0,0 +1,89 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Faça o login do plugin com autenticação LDAP. Fornece uma alternativa para o plugin padrão de login do Matomo.",
+ "MenuLdap": "Usuários LDAP",
+ "LoadUser": "Carregar Usuário através de LDAP",
+ "LoadUserDescription": "Você pode utilizar este formulário para sincronizar um único usuário em LDAP a partir da Interface do Usuário.",
+ "LoadUserCommandDesc": "Para sincronizar mais usuários, utilize o comando %s.",
+ "Settings": "Configurações do LDAP",
+ "AliasField": "Utilizar Campo Pseudônimo",
+ "BaseDn": "DN Base",
+ "LdapPort": "Server Port",
+ "MailField": "Campo de Endereço de E-mail",
+ "ServerUrl": "URL do Server",
+ "UserIdField": "Use Campo ID",
+ "AliasFieldDescription": "O atributo LDAP usado para gerar um apelido para um usuário Matomo, e.g., \"cn\".",
+ "MailFieldDescription": "O atributo LDAP contendo o endereço de e-mail do usuário, e.g. \"mail\".",
+ "UserIdFieldDescription": "O atributo LDAP que contém o nome de usuário de um usuário, e.g., \"uid\", ou para o Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "Usuário \"%s\" não encontrado!",
+ "NoUserName": "Não há esse nome de usuário!",
+ "UsernameSuffix": "Sufixo do Endereço de E-mail",
+ "UsernameSuffixDescription": "Anexado a nomes de usuários para gerar endereços de e-mail para os usuários que não têm nenhum em LDAP. Configure para, e.g., \"@localhost.com\".",
+ "AdminUser": "Nome de Usuário Bind LDAP",
+ "AdminUserDescription": "Usuário com acesso a outras entradas de usuários, ou em branco para ligação anônima.",
+ "AdminPass": "Senha LDAP",
+ "MemberOf": "Grupo do Usuário Requerido",
+ "MemberOfDescription": "Um grupo em que o usuário é obrigado a ser um membro de, e.g. \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "Nota: Esta configuração utiliza o filtro LDAP memberOf=?. Alguns servidores LDAP requerem configurações adicionais para que isso funcione.",
+ "Filter": "Filtro de Busca LDAP",
+ "FilterDescription": "Um filtro LDAP utilizado para determinar quais entidades estão autorizadas a serem utilizadas para autenticação, e.g. \"(objectClass=person)\".",
+ "Kerberos": "Use Autenticador do Servidor Web (e.g. Kerberos SSO)",
+ "LdapUserAdded": "Usuário LDAP adicionado com sucesso!",
+ "LdapFunctionsMissing": "A extensão PHP LDAP parece não estar habilitada. Ela é necessária para este plugin. Por favor, instale-a.",
+ "CannotConnectToServer": "Não foi possível conectar com o servidor LDAP",
+ "CannotConnectToServers": "Não foi possível conectar-se a nenhum dos %s servidores LDAP.",
+ "LDAPServers": "Servidores LDAP",
+ "OneUser": "1 usuário",
+ "MemberOfCount": "%s são membros deste grupo",
+ "FilterCount": "%s conferem com este filtro",
+ "Test": "Teste",
+ "InvalidFilter": "Filtro inválido",
+ "FirstNameField": "Campo Primeiro Nome",
+ "FirstNameFieldDescription": "O atributo LDAP contendo o primeiro nome do usuário",
+ "LastNameField": "Campo ùltimo Nome",
+ "LastNameFieldDescription": "O atributo LDAP contendo o último nome do usuário",
+ "FirstLastNameForAlias": "O primeiro e último nome são usados para gerar um apelido, caso não seja encontrado um no LDAP.",
+ "NewUserDefaultSitesViewAccess": "Sites Iniciais com Acesso de Visualização para Novos Usuários",
+ "NewUserDefaultSitesViewAccessDescription": "Se especificado, quando um usuário LDAP é sincronizado pela primeira vez é dado a ele(a) acesso de leitura a esses sites. Deve ser definido para uma lista de IDs para 'todos'.",
+ "EnableLdapAccessSynchronization": "Permitir Sincronização de Acesso do Usuário a partir do LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Se ativado, os níveis de acesso do usuário são determinadas por atributos LDAP personalizados. Nota: Para utilizar esta função, você terá de modificar o esquema do servidor LDAP.",
+ "LdapViewAccessField": "Campo Acesso de Visualização LDAP",
+ "LdapViewAccessFieldDescription": "O atributo LDAP personalizado que determina quais os sites que um usuário tem acesso a visualização.",
+ "LdapAdminAccessField": "Campo Acesso do Admin LDAP",
+ "LdapAdminAccessFieldDescription": "O atributo LDAP personalizado que determina quais os sites que um usuário tem acesso administrativo.",
+ "LdapSuperUserAccessField": "Campo de Acesso de Super Usuário LDAP",
+ "LdapSuperUserAccessFieldDescription": "O atributo LDAP personalizado que determina se um usuário é ou não um superusuário.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Delimitador de Acesso do Usuário às Especificações de Atributos do Servidor",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "A seqüência utlizada para delimitar as especificações do servidor em um atributo de acesso do usuário. Se definido como ';', o atributo de acesso será esperado como, e.g., '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Atributo do Servidor de Acesso do Usuário & Separador de Lista do Site",
+ "LdapUserAccessAttributeServerSeparatorDescription": "A seqüência utilizada para separar IDs da instância do servidor Matomo com listas de ID do site. Se definido como ':', o atributo de acesso será esperado parecer com '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Nome Especial para esta Instância do Matomo",
+ "ThisMatomoInstanceNameDescription": "Um nome especial usado para identificar esta instância Matomo em um atributo de acesso. Se nenhum for especificado, supomos que seja a URL base para este Matomo.",
+ "UnsupportedPasswordReset": "Não é possível redefinir a senha do usuário. Por favor, entre em contato com um administrador.",
+ "UserSyncSettings": "Configurações de Sincronização do Usuário",
+ "AccessSyncSettings": "Configurações de Sincronização de Acesso",
+ "SynchronizeUsersAfterLogin": "Sincronizar Usuários Após Login Efetuado com Sucesso",
+ "SynchronizeUsersAfterLoginDescription": "Se ativado, os detalhes do usuário em LDAP serão sempre sincronizados após um login bem-sucedido.",
+ "SynchronizeUsersAfterLoginDescription2": "Se desativado, o plugin não irá sincronizar informações do usuário após o login, a menos que a autenticação LDAP seja ativada. Quando possível, conexões LDAP serão inteiramente evitadas.",
+ "UseLdapForAuthentication": "Sempre Utilize LDAP para Autenticação",
+ "UseLdapForAuthenticationDescription": "Se desativada, as senhas de usuário serão armazenados no banco de dados do Matomo, além do LDAP. Isto irá resultar em logins mais rápidos, mas é menos seguro.",
+ "PasswordField": "Campo de Senha do Usuário",
+ "PasswordFieldDescription": "Nome do atributo LDAP que contém a senha de um usuário, por exemplo, 'userPassword' ou 'unicodePwd'.",
+ "PasswordFieldDescription2": "Se 'Sempre Usar LDAP para Autenticação' estiver ativado e \"Gerar token_auth aleatórios para Novos Usuários\" estiver desativado, o valor deste campo em LDAP deve ser mascarado ou a senha do usuário criptografada.",
+ "ReadMoreAboutAccessSynchronization": "Para saber mais sobre a sincronização de acesso do usuário, %1$sleia nossos docs%2$s.",
+ "ExpectedLdapAttributes": "Atributos LDAP esperados",
+ "ExpectedLdapAttributesPrelude": "Com esta configuração o LoginLdap vai supor atributos em LDAP que se parecem",
+ "NetworkTimeout": "Tempo Limite de Solicitação da Rede LDAP (em segs)",
+ "NetworkTimeoutDescription": "O limite de tempo em segundos para uma solicitação da rede LDAP.",
+ "NetworkTimeoutDescription2": "Quanto maior o tempo de espera, mais tempo os usuários tem que esperar antes do Matomo poder registrá-los por meio do LDAP, se um ou mais de seus servidores deixar de responder.",
+ "Go": "Ir",
+ "LoginPluginEnabledWarning": "mbos os plugins %1$s e %2$s estão ativados! Isto fará com que logins para os usuários LDAP falhem. Por favor, desative o plugin %1$s.",
+ "MemberOfField": "Campo memberOf LDAP",
+ "MemberOfFieldDescription": "Campo utilizado pelo seu LDAP para indicar membro, por padrão \"memberOf\"",
+ "LdapUrlPortWarning": "A opção de porta é ignorada quando o nome do servidor é definido como uma URL LDAP (isto é, ldap:\/\/localhost\/ ao invés de localhost).",
+ "UpdateFromPre300Warning": "Já que você atualizou a partir de uma versão pré-3.0.0, você provavelmente deve manter esta opção desmarcada. Antes da versão 3.0.0 as senhas de usuários foram armazenadas tanto no LDAP quanto no Banco de Dados do Matomo. Esta opção deve ser desmarcada para que os usuários adicionados antes da atualização sejam capazes de acessar suas contas.",
+ "MobileAppIntegrationNote": "Nota: Se você planeja usar LDAP com o aplicativo móvel oficial, você deve manter esta opção desmarcada. Atualmente, o aplicativo móvel não pode autenticar os usuários se as senhas de usuários não estiverem armazenadas no Banco de Dados do Matomo.",
+ "PasswordFieldHelp": "Digite uma senha para substituir a existente ou deixe em branco para não modificar.",
+ "LdapUserCantChangePassword": "Alteração de senha não é suportado para usuários LDAP. Ao usar LDAP, as configurações de usuário são gerenciados diretamente em LDAP. Para mais informações, entre em contato com o administrador do servidor LDAP ou o administrador do Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/pt.json b/lang/pt.json
new file mode 100644
index 0000000..50a0b73
--- /dev/null
+++ b/lang/pt.json
@@ -0,0 +1,6 @@
+{
+ "LoginLdap": {
+ "MenuLdap": "Users da LDAP",
+ "Test": "Teste"
+ }
+} \ No newline at end of file
diff --git a/lang/ro.json b/lang/ro.json
new file mode 100644
index 0000000..3907deb
--- /dev/null
+++ b/lang/ro.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Test"
+ }
+} \ No newline at end of file
diff --git a/lang/ru.json b/lang/ru.json
new file mode 100644
index 0000000..ce252cb
--- /dev/null
+++ b/lang/ru.json
@@ -0,0 +1,54 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Плагин входа с LDAP аутентификацией. Является альтернативой стандартному плагину Matomo.",
+ "MenuLdap": "Пользователи LDAP",
+ "LoadUser": "Загрузить пользователя из LDAP",
+ "LoadUserCommandDesc": "Для синхронизации других пользоветелей используйте команду %s.",
+ "Settings": "Настройки LDAP",
+ "AliasField": "Поле псевдоним пользователя",
+ "BaseDn": "Base DN",
+ "LdapPort": "Порт сервера",
+ "MailField": "Поле E-mail адрес",
+ "ServerUrl": "URL сервера",
+ "UserIdField": "Поле ID пользователя",
+ "AliasFieldDescription": "LDAP атрибут, используемый для привязывания к пользователю Matomo, например, \"cn\".",
+ "MailFieldDescription": "Атрибут LDAP, содержащий адрес электронной почты пользователя, например \"mail\".",
+ "UserNotFound": "Пользователь \"%s\" не найден!",
+ "NoUserName": "Не задано имя пользователя!",
+ "UsernameSuffix": "Суффикс E-mail адреса",
+ "AdminUserDescription": "Пользователь, с доступом к записям других пользователей, или пусто для анонимной связи.",
+ "AdminPass": "LDAP пароль",
+ "MemberOf": "Требуется группа пользователя",
+ "Filter": "Поисковый фильтр LDAP",
+ "FilterDescription": "LDAP фильтр, используемый чтобы определить какие сущности разрешено использовать для авторизации, например, \"(objectClass=person)\".",
+ "Kerberos": "Использовать авторизацию в веб сервере (например, Kerberos SSO)",
+ "LdapUserAdded": "LDAP пользователь успешно добавлен!",
+ "LdapFunctionsMissing": "Похоже, что расширение LDAP для PHP не включено. Оно необходимо для этого плагина. Пожалуйста, установите его.",
+ "CannotConnectToServer": "Невозможно соединиться с LDAP сервером.",
+ "CannotConnectToServers": "Не возможно подключиться ни к одному из %s LDAP серверов.",
+ "LDAPServers": "LDAP серверы",
+ "OneUser": "1 пользователь",
+ "MemberOfCount": "%s являются членом этой группы.",
+ "FilterCount": "%s соотвествует данному фильтру.",
+ "Test": "Тест",
+ "InvalidFilter": "Неправильный фильтр.",
+ "ServerName": "Имя сервера",
+ "FirstNameField": "Поле с именем",
+ "FirstNameFieldDescription": "LDAP атрибут, содержащий имя пользователя.",
+ "LastNameField": "Поле с фамилией",
+ "LastNameFieldDescription": "LDAP атрибут, содержащий фамилию пользователя.",
+ "ThisMatomoInstanceName": "Специальное имя для этого экземпляра Matomo",
+ "UnsupportedPasswordReset": "Не удалось переустановить пароль этого пользователя. Пожалуйста, обратитесь к администратору.",
+ "UserSyncSettings": "Настройки синхронизации пользователей",
+ "AccessSyncSettings": "Настройки синхронизации доступа",
+ "SynchronizeUsersAfterLogin": "Синхронизировать пользователей после успешного входа",
+ "UseLdapForAuthentication": "Всегда использовать LDAP для идентификации",
+ "PasswordField": "Поле пароль пользователя",
+ "ReadMoreAboutAccessSynchronization": "Чтобы узнать больше о синхронизации прав пользователей, %1$sпрочитайте документацию%2$s.",
+ "ExpectedLdapAttributes": "Ожидаемые LDAP атрибуты",
+ "Go": "Вперед",
+ "MemberOfField": "LDAP поле memberOf",
+ "MobileAppIntegrationNote": "Внимание: Если Вы планируете использовать LDAP с оффициальным мобильным приложением. Вы должны оставить эту опцию невыбранной. На данный момент мобильное приложение не может авторизовать пользователей, если пароли пользователей не сохранены в базе данных Matomo.",
+ "PasswordFieldHelp": "Введите пароль для замены существующего или оставьте пустым, чтобы не менять."
+ }
+} \ No newline at end of file
diff --git a/lang/sk.json b/lang/sk.json
new file mode 100644
index 0000000..6868499
--- /dev/null
+++ b/lang/sk.json
@@ -0,0 +1,6 @@
+{
+ "LoginLdap": {
+ "LdapPort": "Port servera",
+ "Test": "Test"
+ }
+} \ No newline at end of file
diff --git a/lang/sl.json b/lang/sl.json
new file mode 100644
index 0000000..3907deb
--- /dev/null
+++ b/lang/sl.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Test"
+ }
+} \ No newline at end of file
diff --git a/lang/sq.json b/lang/sq.json
new file mode 100644
index 0000000..9837d6e
--- /dev/null
+++ b/lang/sq.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Shtojcë hyrjesh me mirëfilltësim LDAP. Ofron një alternativë ndaj shtojcës së Hyrjeve standarde Matomo.",
+ "MenuLdap": "Përdorues LDAP",
+ "LoadUser": "Ngarko Përdorues prej LDAP-i",
+ "LoadUserDescription": "Këtë formular mund ta përdorni për të njëkohësuar një përdorues të vetëm te LDAP që nga UI.",
+ "LoadUserCommandDesc": "Që të njëkohësoni më tepër përdorues, përdorni urdhrin %s.",
+ "Settings": "Rregullime LDAP",
+ "AliasField": "Fushë Aliasi Përdoruesi",
+ "BaseDn": "DN Bazë",
+ "LdapPort": "Portë Shërbyesi",
+ "MailField": "Fushë Adrese Email",
+ "ServerUrl": "URL Shërbyesi",
+ "UserIdField": "Fushë ID-je Përdoruesi",
+ "AliasFieldDescription": "Atributi LDAP i përdorur për të prodhuar alias përdoruesi Matomo-je, p.sh. \"cn\".",
+ "MailFieldDescription": "Atributi LDAP që përmban adresën email të një përdoruesi, p.sh. \"mail\".",
+ "UserIdFieldDescription": "Atributi LDAP që përmban emrin e përdoruesit të një përdoruesi, p.sh. \"uid\", ose për Drejtori Aktive: \"userPrincipalName\".",
+ "UserNotFound": "Përdoruesi \"%s\" nuk u gjet!",
+ "NoUserName": "S’u dha emër përdoruesi!",
+ "UsernameSuffix": "Prapashtesë Adrese Email",
+ "UsernameSuffixDescription": "U shtohet emrave të përdoruesve për të prodhuar adresa email për përdoruesit që nuk kanë të tillë në LDAP. E caktuar, për shembull, si, \"@localhost.com\".",
+ "AdminUser": "Emër përdoruesi Bind LDAP",
+ "AdminUserDescription": "Përdorues me hyrje te zëra të tjerë përdoruesi, ose e zbrazët për lidhje anonime.",
+ "AdminPass": "Fjalëkalim LDAP",
+ "MemberOf": "Grup i Domosdoshëm Përdoruesi",
+ "MemberOfDescription": "Grup në të cilin është e domosdoshme anëtarësia e një përdoruesi, p.sh. \"cn=Matomo User,cn=Users,dc=organization,dc=com\".",
+ "MemberOfDescription2": "Shënim: Ky rregullim përdor filtrin memberOf=? LDAP. Që kjo të funksionojë, disa shërbyes LDAP lypin rregullim shtesë.",
+ "Filter": "Filtër Kërkimesh LDAP",
+ "FilterDescription": "Një filtër LDAP i përdorur për të përcaktuar cilat njësi lejohen për t’u përdorur për mirëfilltësim, p.sh. \"(objectClass=person)\".",
+ "Kerberos": "Përdor Mirëfilltësim Shërbyesi Web (p.sh. Kerberos SSO)",
+ "KerberosDescription": "Në u aktivizoftë, shtojca do të kontrollojë ndryshoren $_SERVER['REMOTE_USER'] dhe do të marrë si të mirëqenë se për përdoruesin është bërë tashmë mirëfilltësimi. Nëse $_SERVER['REMOTE_USER'] s’është i pranishëm, krejt përdoruesit mirëfilltësohen sipas rregullimeve të tjera.",
+ "LdapUserAdded": "Përdoruesi LDAP u shtua me sukses!",
+ "LdapFunctionsMissing": "Zgjerimi PHP LDAP s’duket se është i aktivizuar. Është i domosdoshëm për këtë shtojcë, ju lutemi, instalojeni.",
+ "CannotConnectToServer": "S’u lidh dot me shërbyesin LDAP.",
+ "CannotConnectToServers": "S’u lidh dot në ndonjë nga %s shërbyesit LDAP.",
+ "LDAPServers": "Shërbyes LDAP",
+ "OneUser": "1 përdorues",
+ "MemberOfCount": "%s është anëtar i këtij grupi.",
+ "FilterCount": "%s kanë përputhje me këtë filtër.",
+ "Test": "Provë",
+ "InvalidFilter": "Filtër i pavlefshëm.",
+ "ServerName": "Emër shërbyesi",
+ "FirstNameField": "Fushë për Emrin",
+ "FirstNameFieldDescription": "Atributi LDAP që përmban emrin e një përdoruesi.",
+ "LastNameField": "Fushë për Mbiemrin",
+ "LastNameFieldDescription": "Atributi LDAP që përmban mbiemrin e një përdoruesi.",
+ "FirstLastNameForAlias": "Emri dhe mbiemri përdoren për të prodhuar një alias, nëse s’gjendet një i tillë në LDAP.",
+ "NewUserDefaultSitesViewAccess": "Sajte Fillestarë Me Hyrje Parjesh Për Përdorues të Rinj",
+ "NewUserDefaultSitesViewAccessDescription": "Nëse jepen vlera, kur një përdorues LDAP njëkohësohet për herë të parë, atij ose asaj i jepen të drejta parje për këto sajte. Duhet plotësuar si një listë ID-sh ose t’i jepet vlera 'all'.",
+ "EnableLdapAccessSynchronization": "Aktivizo Njëkohësim Hyrjesh Përdoruesi prej LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Në u aktivizoftë, nivelet e hyrjeve për përdoruesit përcaktohen nga atribute vetjakë LDAP. Shënim: Që të përdorni këtë veçori, do t’ju duhet të ndryshoni skemën e shërbyesit tuaj LDAP.",
+ "LdapViewAccessField": "Fusha LDAP për Hyrje Parjesh",
+ "LdapViewAccessFieldDescription": "Atributi vetjak LDAP që përcakton se te cilët sajte mund të hyjë një përdorues për t’i parë.",
+ "LdapAdminAccessField": "Fusha LDAP për Hyrje Përgjegjësi",
+ "LdapAdminAccessFieldDescription": "Atributi vetjak LDAP që përcakton se te cilët sajte mund të hyjë si përgjegjës një përdorues.",
+ "LdapSuperUserAccessField": "Fusha LDAP për Hyrje Superpërdoruesi",
+ "LdapSuperUserAccessFieldDescription": "Atributi vetjak LDAP që përcakton nëse një përdorues është apo jo superpërdorues.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Kufizues Specifikimesh Shërbyesi në Atribut Hyrjesh Përdoruesi",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Vargu i përdorur për të kufizuar specifikime shërbyesi brenda një atributi hyrjesh përdoruesi. Nëse caktohet si ';', atributi i hyrjeve do të pritet të duket si '<server-spec>;<server-spec>;…'.",
+ "LdapUserAccessAttributeServerSeparator": "Ndarës Shërbyesi & Listë Sajti në Atribut Hyrjesh Përdoruesi",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Vargu i përdorur për të ndarë ID instancash shërbyesi Matomo me lista ID-sh sajti. Nëse caktohet si ':', atributi i hyrjeve do të pritet të duket si '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Emër Special Për Këtë Instancë Matomo",
+ "ThisMatomoInstanceNameDescription": "Një emër special i përdorur për të identifikuar këtë instancë Matomo në një atribut hyrjesh. Nëse s’jepet ndonjë, presim URL-në bazë të kësaj instance Matomo.",
+ "UnsupportedPasswordReset": "S’ricaktohet dot fjalëkalimi i këtij përdoruesi, ju lutemi, lidhuni me një nga përgjegjësit.",
+ "UserSyncSettings": "Përdor Rregullime Njëkohësimi",
+ "AccessSyncSettings": "Hyni te Rregullime Njëkohësimi",
+ "SynchronizeUsersAfterLogin": "Njëkohësoji Përdoruesit Pas Hyrjes Me Sukses",
+ "SynchronizeUsersAfterLoginDescription": "Në u aktivizoftë, hollësitë e përdoruesi në LDAP do të jenë përherë të njëkohësuara, pas një hyrjeje të suksesshme.",
+ "SynchronizeUsersAfterLoginDescription2": "Në u çaktivizoftë, shtojca nuk do të kryejë mirëfilltësim të përdoruesit pas hyrjes, veç në qoftë i aktivizuar mirëfilltësimi LDAP. Kur është e mundur, lidhjet LDAP do të shmangen tërësisht.",
+ "UseLdapForAuthentication": "Përdor Përherë LDAP për Mirëfilltësim",
+ "UseLdapForAuthenticationDescription": "Në u çaktivizoftë, fjalëkalimet e përdoruesve do të ruhen te BD-ja e Matomo-s, përveç se edhe te LDAP. Kjo do të shpjerë në hyrje më të shpejta, por është më pak e sigurt.",
+ "PasswordField": "Fushë Fjalëkalimi Përdoruesi",
+ "PasswordFieldDescription": "Emri i atributit LDAP që përmban fjalëkalimin e një përdoruesi, p.sh. 'userPassword' ose 'unicodePwd'.",
+ "PasswordFieldDescription2": "Nëse 'Përdor Përherë LDAP për Mirëfilltësim' është e aktivizuar dhe 'Prodho token_auth Kuturu Për Përdorues të Rinj' është e çaktivizuar, vlera e kësaj fushe në LDAP duhet të jetë fjalëkalimi i përdoruesit, i trajtuar me hash ose i fshehtëzuar.",
+ "ReadMoreAboutAccessSynchronization": "Për të mësuar më tepër mbi njëkohësim përdoruesish, %1$slexoni dokumentimin tonë%2$s.",
+ "ExpectedLdapAttributes": "Atribute LDAP të pritshëm",
+ "ExpectedLdapAttributesPrelude": "Me këtë formësim, LoginLdap do të presë për atribute në LDAP që duken si",
+ "NetworkTimeout": "Kohë Mbarimi Kërkese Rrjeti LDAP (në sekonda)",
+ "NetworkTimeoutDescription": "Kufiri kohor në sekonda për një kërkesë rrjeti LDAP.",
+ "NetworkTimeoutDescription2": "Sa më e madhe koha e mbarimit, aq më shumë kohë mund t’u duhet të presin përdoruesve tuaj, përpara se Matomo të mund të bëjë për ta hyrjen përmes LDAP-je, nëse një ose disa nga shërbyesit tuaj reshtin së përgjigjuri.",
+ "Go": "Jepi",
+ "LoginPluginEnabledWarning": "Janë të aktivizuara që të dyja shtojcat, %1$s dhe %2$s! Kjo do të shkaktojë dështimin e hyrjeve të përdoruesve LDAP, ju lutemi, çaktivizoni shtojcën %1$s.",
+ "MemberOfField": "Fusha LDAP memberOf",
+ "MemberOfFieldDescription": "Fushë e përdorur nga LDAP-ja juaj për të treguar anëtarësi, si parazgjedhje \"memberOf\"",
+ "LdapUrlPortWarning": "Mundësia portë shpërfillet, kur si strehëemër shërbyesi është caktuar një URL LDAP (domethënë, ldap:\/\/localhost\/, në vend se localhost).",
+ "UpdateFromPre300Warning": "Ngaqë e përditësuat prej një versioni para-3.0.0, mundet të duhej të mos i vini shenjë kësaj mundësie. Para versionit 3.0.0, fjalëkalimet e përdoruesve depozitoheshin si në LDAP, ashtu edhe në BD-në e Matomo-s. Kjo mundësi duhet të lihet e pazgjedhur, në mënyrë që përdoruesit e shtuar para përmirësimit të jenë në gjendje të bëjnë hyrjen.",
+ "MobileAppIntegrationNote": "Shënim: Nëse keni në plan të përdorni LDAP me aplikacionin zyrtar për celular, duhet të mos i vini shenjë kësaj mundësie. Hëpërhë, aplikacioni për celular s’mund të bëjë mirëfilltësim përdoruesish, nëse fjalëkalimet nuk janë depozituar në BD-në e Matomo-s.",
+ "PasswordFieldHelp": "Jepni një fjalëkalim që të mbishkruhet fjalëkalimi ekzistues, ose lëreni të zbrazët që të mos bëhen ndryshime.",
+ "LdapUserCantChangePassword": "Ndryshimi i fjalëkalimit nuk mbulohet për përdorues LDAP. Kur përdorni LDAP, rregullimet tuaja të përdoruesit administrohen drejtpërsëdrejti nga LDAP-ja. Për më tepër të dhëna, ju lutemi, lidhuni me përgjegjësin e shërbyesit tuaj LDAP ose përgjegjësin e instalimit tuaj Matomo."
+ }
+} \ No newline at end of file
diff --git a/lang/sr.json b/lang/sr.json
new file mode 100644
index 0000000..7b995b0
--- /dev/null
+++ b/lang/sr.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Dodatak za prijavljivanje na sistem koji koristi LDAP. Alternativa standardnom Matomo dodatku za prijavu.",
+ "MenuLdap": "LDAP korisnici",
+ "LoadUser": "Učitaj korisnika iz LDAP-a",
+ "LoadUserDescription": "Možete upotrebiti ovaj formular kako biste sinhronizovali LDAP korisnika.",
+ "LoadUserCommandDesc": "Ako želite da sinhronizujete više korisnika, koristite komandu %s.",
+ "Settings": "LDAP podešavanja",
+ "AliasField": "Polje alias",
+ "BaseDn": "Osnovni DN",
+ "LdapPort": "Port servera",
+ "MailField": "Polje elektronska adresa",
+ "ServerUrl": "URL servera",
+ "UserIdField": "Polje korisnički ID",
+ "AliasFieldDescription": "LDAP atribut korišćen za generisanje korisnikovog Matomo aliasa, na primer \"cn\".",
+ "MailFieldDescription": "LDAP atribut koji sadrži korisnikovu elektronsku adresu, na primer \"mail\".",
+ "UserIdFieldDescription": "LDAP atribut koji sadrži korisnikovo korisničko ime, na primer \"uid\" ili \"userPrincipalName\".",
+ "UserNotFound": "Korisnik \"%s\" nije nađen",
+ "NoUserName": "Korisničko ime nije zadato",
+ "UsernameSuffix": "Sufiks elektronske adrese",
+ "UsernameSuffixDescription": "Dodato na korisnička imena kako bi se generisala elektronska adresa korisnicima koji je nemaju na LDAP-u. Postavite na npr. \"@localhost.com\".",
+ "AdminUser": "LDAP Bind korisničko ime",
+ "AdminUserDescription": "Korisnik sa pristupom ostalim korisničkim zapisima, ili ostavite prazno za anonimni bind.",
+ "AdminPass": "LDAP lozinka",
+ "MemberOf": "Korisnička grupa",
+ "MemberOfDescription": "Grupa kojoj korisnik mora da pripada, na primer",
+ "MemberOfDescription2": "Ovo podešavanje koristi memberOf=? LDAP filter. Neki LDAP serveri zahtevaju dodatna podešavanja da bi to radilo.",
+ "Filter": "LDAP filtar pretraživanja",
+ "FilterDescription": "LDAP filtar koji se koristi kako bi se odredilo kojim entitetima je dozvoljeno da se prijave, na primer \"(objectClass=person)\".",
+ "Kerberos": "Koristi Web Server Auth (npr. Kerberos SSO)",
+ "KerberosDescription": "Ukoliko je omogućeno, dodatak će proveriti $_SERVER['REMOTE_USER'] promenljivu i pretpostaviti da je korisnik već verifikovan. Ako promenljiva $_SERVER['REMOTE_USER'] nije postavljena, korisnici će biti verifikovani prema ostalim podešavanjima.",
+ "LdapUserAdded": "LDAP korisnik je uspešno ubačen!",
+ "LdapFunctionsMissing": "Izgleda da PHP LDAP proširenje nije uključeno. Molimo vas da ga instalirate pošto je neophodno za funkcionisanje ovog dodatka.",
+ "CannotConnectToServer": "Ne mogu da se povežem na LDAP server.",
+ "CannotConnectToServers": "Ne mogu da se povežem ni na jedan od %s LDAP servera.",
+ "LDAPServers": "LDAP serveri",
+ "OneUser": "1 korisnik",
+ "MemberOfCount": "%s je član ove grupe.",
+ "FilterCount": "%s je prepoznat ovim filtrom.",
+ "Test": "Test",
+ "InvalidFilter": "Filter je nevažeći.",
+ "ServerName": "Naziv servera",
+ "FirstNameField": "Polje ime",
+ "FirstNameFieldDescription": "LDAP atribut koji sadrži korisnikovo ime.",
+ "LastNameField": "Polje prezime",
+ "LastNameFieldDescription": "LDAP atribut koji sadrži korisnikovo prezime.",
+ "FirstLastNameForAlias": "Ime i prezime se koriste kako bi se generisao alias ukoliko ga nema na LDAP-u.",
+ "NewUserDefaultSitesViewAccess": "Inicijalni sajtovi kojima novi korisnici imaju pristup",
+ "NewUserDefaultSitesViewAccessDescription": "Ukoliko je ovo popunjeno, onda kada se LDAP korisnik po prvi put sinhronizuje, biva mu dodeljen pristup tim sajtovima. Treba da bude podešeno kao lista ID-jeva ili 'sve'.",
+ "EnableLdapAccessSynchronization": "Omogući sinhronizaciju korisničkog pristupa sa LDAP-a",
+ "EnableLdapAccessSynchronizationDescription": "Ukoliko je omogućeno, korisnički nivoi pristupa su određeni LDAP atributima. Imajte na umu da, ako želite da koristite ovu mogućnost, onda ćete morati da izmenite serversku šemu vašeg LDAP-a.",
+ "LdapViewAccessField": "Polje LDAP pristup",
+ "LdapViewAccessFieldDescription": "LDAP atribut koji određuje kojim sajtovima korisnik ima pristup.",
+ "LdapAdminAccessField": "Polje LDAP administracija",
+ "LdapAdminAccessFieldDescription": "LDAP atribut koji određuje koje sajtove korisnik može da administrira.",
+ "LdapSuperUserAccessField": "Polje LDAP superkorisnik",
+ "LdapSuperUserAccessFieldDescription": "LDAP atribut koji određuje da li je korisnik superkorisnik ili ne.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Delimiter za specifikaciju atributa korisničkih pristupa",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Znak koji će da služi kao delimiter atributa korisničkih pristupa. Ako je postavljen na ';', lista atributa će izgledati poput '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Separator liste sajtova i atributa korisničkih pristupa",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Znak koji će da razdvaja Matomo servere i sajtove. Ako je postavljen na ';', lista će izgledati poput '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Specijalno ime ove Matomo instance",
+ "ThisMatomoInstanceNameDescription": "Specijalno ime koje će služiti za identifikaciju ove Matomo instance. Ukoliko nije navedeno, očekuje se da to bude osnovni URL na kojem se nalazi Matomo.",
+ "UnsupportedPasswordReset": "Nije moguće resetovati lozinku ovog korisnika, molimo vas da kontaktirate administratora.",
+ "UserSyncSettings": "Podešavanja sinhronizacije korisnika",
+ "AccessSyncSettings": "Podešavanja sinhronizacije pristupa",
+ "SynchronizeUsersAfterLogin": "Sinhronizuj korisnike nakon uspešne prijave",
+ "SynchronizeUsersAfterLoginDescription": "Ako je uključeno, detalji o korisniku u LDAP-u će uvek biti sinhronizovani nakon uspešne prijave.",
+ "SynchronizeUsersAfterLoginDescription2": "Ako je isključeno, dodatak neće sinhronizovati korisničke podatke sem ako LDAP autentifikacija nije omogućena. Kad god je moguće, LDAP konekcije će biti izbegnute.",
+ "UseLdapForAuthentication": "Uvek koristi LDAP za prijavljivanje",
+ "UseLdapForAuthenticationDescription": "Ako je isključeno, korisnička lozinka će biti sačuvana u Matomo bazi podataka. Ovo rezultuje bržom prijavom ali je manje bezbedno.",
+ "PasswordField": "Polje lozinka",
+ "PasswordFieldDescription": "Naziv LDAP atributa koji sadrži korisnikovu lozinku, na primer 'userPassword' ili 'unicodePwd'.",
+ "PasswordFieldDescription2": "Ako je uključena opcija 'Uvek koristi LDAP za prijavljivanje' a opcija 'Generiši slučajan token_auth za nove korisnike' isključena, vrednost ovog polja u LDAP-u mora biti hešovana ili kriptovana lozinka korisnika.",
+ "ReadMoreAboutAccessSynchronization": "Ako želite više da naučite o sinhronizaciji korisničkih pristupa, %1$spročitajte dokumentaciju%2$s.",
+ "ExpectedLdapAttributes": "Očekivani LDAP atributi",
+ "ExpectedLdapAttributesPrelude": "Sa ovim podešavanjima, LoginLdap očekuje LDAP atribute koji izgledaju ovako:",
+ "NetworkTimeout": "Vremenski istek LDAP mrežnog zahteva (u sekundama)",
+ "NetworkTimeoutDescription": "Vremenski limit u sekundama za LDAP mrežni zahtev.",
+ "NetworkTimeoutDescription2": "Što je veći vremenski limit, više vremena korisnik može da čeka pre nego što ga Matomo prijavi preko LDAP-a ako jedan ili više vaših servera postanu nedostupni.",
+ "Go": "Kreni",
+ "LoginPluginEnabledWarning": "I %1$s i %2$s su uključeni, što će dovesti do toga da vaši LDAP korisnici ne mogu da se prijave. Molimo vas da isključite dodatak %1$s.",
+ "MemberOfField": "Polje LDAP",
+ "MemberOfFieldDescription": "Polje koje LDAP koristi kako bi čuvao informacije o pripadnosti grupama. Podrazumevano to je \"memberOf\".",
+ "LdapUrlPortWarning": "Broj porta se ignoriše kada je naziv hosta predstavljen LDAP URL-om (npr. ldap:\/\/localhost\/ umesto of localhost).",
+ "UpdateFromPre300Warning": "Pošto ste nadogradili Matomo sa verzije pre 3.0.0, ovu opciju bi trebalo da držite isključenu. Pre verzije 3.0.0, lozinke su čuvane i na LDAP-u i u Matomo bazi podataka. Ova opcija mora biti isključena kako bi korisnici dodati pre nadogradnje mogli da se prijave.",
+ "MobileAppIntegrationNote": "Pažnja: ako planirate da koristite LDAP sa zvaničnom aplikacijom za mobilni, ovu opciju morate držati isključenu. Mobilna aplikacija trenutno ne može da prijavi korisnika ako njegova lozinka nije sačuvana u Matomo bazi podataka.",
+ "PasswordFieldHelp": "Upišite lozinku kako biste promenili prethodnu ili ostavite prazno ako ne želite da je menjate.",
+ "LdapUserCantChangePassword": "Promena lozinke LDAP korisnika nije podržana. Pošto vi koristite LDAP, vašim korisničkim podešavanjima se direktno upravlja u LDAP-u. Za više informacija molimo vas da kontaktirate adminstratora vašeg LDAP servera ili vašeg Matomo administratora."
+ }
+} \ No newline at end of file
diff --git a/lang/sv.json b/lang/sv.json
new file mode 100644
index 0000000..aa1302b
--- /dev/null
+++ b/lang/sv.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Inloggningsplugin med stöd för LDAP-autentisering. Ett alternativ till Matomos vanliga login.",
+ "MenuLdap": "LDAP-användare",
+ "LoadUser": "Läs in användare från LDAP",
+ "LoadUserDescription": "Du kan använda detta formulär för att synkronisera en enstaka LDAP-användare i gränssnittet.",
+ "LoadUserCommandDesc": "För att synkronisera fler användare, använd %s kommandot.",
+ "Settings": "LDAP-inställningar",
+ "AliasField": "Användar-aliasfält",
+ "BaseDn": "Base DN",
+ "LdapPort": "Serverport",
+ "MailField": "Fält för e-postadress",
+ "ServerUrl": "Server URL",
+ "UserIdField": "Fält för användar-ID",
+ "AliasFieldDescription": "LDAP-attribut för att skapa en användares Matomo-alias, t.ex. \"cn\".",
+ "MailFieldDescription": "LDAP-attribut för användarens e-postadress, t.ex. \"mail\".",
+ "UserIdFieldDescription": "LDAP-attribut för användarens användarnamn, t.ex. \"uid\", eller för Active Directory: \"userPrincipalName\".",
+ "UserNotFound": "Användare \"%s\" hittades inte!",
+ "NoUserName": "Användarnamn saknas!",
+ "UsernameSuffix": "Suffix för e-postadress",
+ "UsernameSuffixDescription": "Tillägg till användarnamnet för att generera e-postadresser för användare som inte har någon i LDAP. Sätt till t.ex. \"@localhost.com\".",
+ "AdminUser": "LDAP Bind Username",
+ "AdminUserDescription": "Användare med tillgång till andra användarposter eller tomt för anonym koppling.",
+ "AdminPass": "LDAP-lösenord",
+ "MemberOf": "Nödvändig användargrupp",
+ "MemberOfDescription": "En grupp som användaren måste tillhöra t.ex. \"cn=Matomo Användare,cn=Användare,dc=Organisation,dc=com\".",
+ "MemberOfDescription2": "Notering: Denna inställning använder filtret memberOf=? LDAP. Några LDAP servrar kräver extra konfiguration för att detta ska fungera.",
+ "Filter": "LDAP sökfilter",
+ "FilterDescription": "Ett LDAP-filter som används för att avgöra vilka entiteter som är tillåtna för autentisering, t.ex. \"(objectClass=person)\".",
+ "Kerberos": "Använd Web Server Auth (t.ex. Kerberos SSO)",
+ "KerberosDescription": "Om angivet kommer denna plugin att använda variabeln $_SERVER['REMOTE_USER'] och anta att användaren redan är inloggad. Om $_SERVER['REMOTE_USER'] inte finns kommer alla användare att loggas in med den andra inställningen.",
+ "LdapUserAdded": "LDAP-användare kunde läggas till!",
+ "LdapFunctionsMissing": "LDAP-extension i PHP verkar inte vara aktiverad. Den behövs för denna plugin. Vänligen installera\/aktivera den.",
+ "CannotConnectToServer": "Kunde inte ansluta till LDAP-servern.",
+ "CannotConnectToServers": "Kunde inte ansluta till någon av %s LDAP-servrarna.",
+ "LDAPServers": "LDAP-servrar",
+ "OneUser": "1 användare",
+ "MemberOfCount": "%s medlemmar i denna grupp.",
+ "FilterCount": "%s matchas av detta filter.",
+ "Test": "Testa",
+ "InvalidFilter": "Ogiltigt filter.",
+ "ServerName": "Servernamn",
+ "FirstNameField": "Fält för förnamn",
+ "FirstNameFieldDescription": "LDAP-attributet som innehåller användarens förnamn.",
+ "LastNameField": "Fält för efternamn",
+ "LastNameFieldDescription": "LDAP-attributet som innehåller användarens efternamn.",
+ "FirstLastNameForAlias": "För- och efternamn används för att skapa ett alias om inget hittas i LDAP-katalogen.",
+ "NewUserDefaultSitesViewAccess": "Webbplatser med visningsåtkomst för nya användare",
+ "NewUserDefaultSitesViewAccessDescription": "När en LDAP-användare synkroniseras första gången ges användaren visningsåtkomst till dessa webbplatser. Anges som en lista av ID:n eller \"all\".",
+ "EnableLdapAccessSynchronization": "Synkronisera webbplatsbehörigheter från LDAP",
+ "EnableLdapAccessSynchronizationDescription": "Om aktiverad bestäms användarnas accessnivå av anpassade LDAP-attribut. OBS: För att använda denna funktion kommer du att behöva ändra schemat i din LDAP-server.",
+ "LdapViewAccessField": "LDAP-fält för visningsåtkomst",
+ "LdapViewAccessFieldDescription": "Anpassat LDAP-attribut som anger vilka webbplatser användaren har rätt att se.",
+ "LdapAdminAccessField": "Fält för LDAP adminåtkomst",
+ "LdapAdminAccessFieldDescription": "Anpassat LDAP-attribut som anger vila webbplatser användaren har adminbehörighet till.",
+ "LdapSuperUserAccessField": "Fält för LDAP Superuseråtkomst",
+ "LdapSuperUserAccessFieldDescription": "LDAP-attribut som anger om användaren är superuser eller ej.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Användartillståndsattribut serverspecifikationsavgränsare",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Strängen som används för att avgränsa serverspecifikationerna i en användarens tillgångsattribut. Om det är satt till ':' så förväntas tillgångsattributet se ut enligt följande '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Användaretillgångsattribut server & sajtlistan avgränsare",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Stärngen som används för att separera Matomo serverns instans ID med listan över sajt-ID'n. Om det är satt till ':' så förväntas tillgångsattributet att se ut enligt följande '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Anpassat namn för denna Matomoinstans",
+ "ThisMatomoInstanceNameDescription": "Ett särskilt namn som används för att identifiera denna Matomoinstans i accessattribut. Om det inte är angett används bas-URL:en för denna Matomoinstallation.",
+ "UnsupportedPasswordReset": "Kan inte återställa användarens lösenord. Kontakta en administratör.",
+ "UserSyncSettings": "Inställningar för användarsynkronisering",
+ "AccessSyncSettings": "Inställningar för behörighetssynkronisering",
+ "SynchronizeUsersAfterLogin": "Synkronisera användare efter inloggning",
+ "SynchronizeUsersAfterLoginDescription": "Om detta är aktiverat så kommer användardetaljer i LDAP alltid vara synkade efter inloggningen lyckas.",
+ "SynchronizeUsersAfterLoginDescription2": "Om detta är inaktiverat så kommer tillägget inte synka användarinformation efter inloggning, om inte LDAP autentisering är aktiverat. När det är möjligt så kommer LDAP anslutningar undvikas helt.",
+ "UseLdapForAuthentication": "Använd alltid LDAP för autentisering",
+ "UseLdapForAuthenticationDescription": "Om detta är inaktiverat så kommer användarens lösenord lagras i Matomo's databas som ett tillägg för LDAP. Detta kommer resultera i snabbare inloggningar men är mindre säkert.",
+ "PasswordField": "Fält för användarens lösenord",
+ "PasswordFieldDescription": "Namnet för LDAP attributet som innehåller en användares lösenord t.ex. 'userPassword' eller 'unicodePwd'.",
+ "PasswordFieldDescription2": "Om 'Använd alltid LDAP för autentisering' är aktiverat och 'Generera slumpmässiga token_auth för nya användare' är inaktiverat så måste värdet från detta fält i LDAP vara användarens hashade eller krypterade lösenord.",
+ "ReadMoreAboutAccessSynchronization": "För att lära dig mer om användaraccessens synkronisering %1$släs vår dokumentation%2$s.",
+ "ExpectedLdapAttributes": "Förväntade LDAP-attribut",
+ "ExpectedLdapAttributesPrelude": "Med denna konfiguration ommer LoginLdap att förvänta sig LDAP-attribut som ser ut som",
+ "NetworkTimeout": "LDAP Timeout för nätversrequest (i sekunder)",
+ "NetworkTimeoutDescription": "Tidsgränsen i sekunder för en LDAP nätverksförfrågan.",
+ "NetworkTimeoutDescription2": "Ju längre tidsgränsen är ju längre tid behöver dina användare vänta innan Matomo släpper in dom genom LDAP, om en eller flera av dina servrar går ner.",
+ "Go": "Kör",
+ "LoginPluginEnabledWarning": "Både %1$s och %2$s tillägget är aktiverade! Detta gör att inloggningen för LDAP användare slutar fungera, vänligen inaktivera %1$s tillägget.",
+ "MemberOfField": "Fält för LDAP memberOf",
+ "MemberOfFieldDescription": "Fält som användas av din LDAP för att indikera medlemskap, som standard används \"memberOf\"",
+ "LdapUrlPortWarning": "Portalternativet ignoreras när servers värdnamn är satt till en LDAP URL (t.ex. ldap:\/\/localhost\/ istället för localhost).",
+ "UpdateFromPre300Warning": "Eftersom du uppdaterade från en version innan 3.0.0 bör du troligen låta detta alternativet vara utbockat. Före version 3.0.0 så sparades användarnas lösenord både i LDAP och Matomos databas. Detta alternativ måste vara utbockat så att användare som lades till innan uppgraderingen ska kunna logga in.",
+ "MobileAppIntegrationNote": "Notering: Om du planerar att använda LDAP med den officiella mobilappen så ska det här alternativet lämnas utbockat. Som det ser ut nu kan inte mobilappen autentisera användare om användarens lösenord inte lagras i Matomo's databas.",
+ "PasswordFieldHelp": "Skriv in ett lösenord för att skriva över det befintliga eller lämna tomt om du inte vill göra några ändringar.",
+ "LdapUserCantChangePassword": "Ändring av ditt lösenord stöds inte för LDAP användare. Eftersom du använder LDAP så hanteras användarinställningarna direkt i LDAP. För mer information vänligen kontakta din LDAP serveradministratör eller din Matomo administratör."
+ }
+} \ No newline at end of file
diff --git a/lang/th.json b/lang/th.json
new file mode 100644
index 0000000..6be3124
--- /dev/null
+++ b/lang/th.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "คำสั่ง"
+ }
+} \ No newline at end of file
diff --git a/lang/tl.json b/lang/tl.json
new file mode 100644
index 0000000..789f896
--- /dev/null
+++ b/lang/tl.json
@@ -0,0 +1,21 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Ang plugi na pang-login na may kasamang LDAP authentication. Ay nagbibigay ng alternatibo sa pangkaraniwang Matomo Login plugin.",
+ "MenuLdap": "Mga User ng LDAP",
+ "LoadUser": "Mag-load ng user mula sa LDAP",
+ "Settings": "Mga setting ng LDAP",
+ "BaseDn": "Base DN",
+ "LdapPort": "Port ng server",
+ "ServerUrl": "URL ng Server",
+ "UserIdField": "User ID Field",
+ "UserNotFound": "Hindi natagpuan ang user \"%s\"!",
+ "NoUserName": "Walang mga username na ibinigay!",
+ "AdminUser": "Nakaugnay na Username ng LDAP",
+ "AdminPass": "LDAP Password",
+ "LdapUserAdded": "Matagumpay na naidagdag ang user sa LDAP!",
+ "CannotConnectToServer": "Hindi makakonekta sa LDAP server.",
+ "CannotConnectToServers": "Hindi makakonekta sa anumang %s LDAP na server.",
+ "LDAPServers": "LDAP Servers",
+ "Test": "Pagsusulit"
+ }
+} \ No newline at end of file
diff --git a/lang/tr.json b/lang/tr.json
new file mode 100644
index 0000000..6e92849
--- /dev/null
+++ b/lang/tr.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "LDAP kimlik doğrulaması için oturum açma uygulama eki standart Matomo oturum açma uygulama ekine alternatfif olarak kullanılabilir.",
+ "MenuLdap": "LDAP Kullanıcıları",
+ "LoadUser": "LDAP üzerinden kullanıcı yükle",
+ "LoadUserDescription": "Bu form kullanılarak yönetim bölümünden tek bir kullanıcının bilgileri LDAP üzerinden eşitlenebilir.",
+ "LoadUserCommandDesc": "Daha çok sayıda kullanıcıyı eşitlemek için %s komutunu kullanın.",
+ "Settings": "LDAP Ayarları",
+ "AliasField": "Kullanıcı Kısaltması Alanı",
+ "BaseDn": "Base DN",
+ "LdapPort": "Sunucu Kapı Numarası",
+ "MailField": "E-posta Adresi Alanı",
+ "ServerUrl": "Sunucu Adresi",
+ "UserIdField": "Kullanıcı Kodu Alanı",
+ "AliasFieldDescription": "Bir kullanıcının Matomo kısaltmasının oluşturulması için kullanılacak LDAP özniteliği. Örnek: \"cn\".",
+ "MailFieldDescription": "Bir kullanıcın e-posta adresinin bulunduğu LDAP özniteliği. Örnek: \"mail\".",
+ "UserIdFieldDescription": "Bir kullanıcının kullanıcı adının bulunduğu LDAP özniteliği. Örnek: .\"uid\" ya da Aktif Dizin için: \"userPrincipalName\".",
+ "UserNotFound": "\"%s\" kullanıcısı bulunamadı!",
+ "NoUserName": "Herhangi bir kullanıcı adı belirtilmemiş!",
+ "UsernameSuffix": "E-posta Adresi Son Eki",
+ "UsernameSuffixDescription": "Kullanıcı adlarından e-posta adresleri oluşturulurken LDAP üzerinde olmayan ve kullanıcı adına eklenecek son ek. Örnek: \"@localhost.com\".",
+ "AdminUser": "LDAP Bind Kullanıcı Adı",
+ "AdminUserDescription": "Kullanıcınin diğer kullanıcı kayıtlarına erişebileceği şekilde seçin ya da isimsiz bind için boş bırakın.",
+ "AdminPass": "LDAP Parolası",
+ "MemberOf": "Zorunlu Kullanıcı Grubu",
+ "MemberOfDescription": "Kullanıcının üyesi olması istenilen kullanıcı grubu. Örnek: \"cn=Matomo Kullanıcısı,cn=Kullanıcı,dc=kuruluş,dc=com'.",
+ "MemberOfDescription2": "Not: Bu ayar memberOf=? LDAP süzgecini kullanır. Bazı LDAP sunucularında bu özelliğin çalışabilmesi için ek ayarlar yapılması gerekir.",
+ "Filter": "LDAP Arama Süzgeci",
+ "FilterDescription": "Kimlik doğrulama için kullanılacak kayıtların seçilmesini sağlayan LDAP süzgeci. Örnek: \"(objectClass=person)\".",
+ "Kerberos": "Web Sunucu Kimlik Doğrulaması Kullanılsın (Örnek Kerberos SSO)",
+ "KerberosDescription": "Bu seçenek etkinleştirildiğinde uygulama eki $_SERVER['REMOTE_USER'] değişkenini kullanarak kullanıcı kimliğinin zaten doğrulanmış olduğunu varsayar. $_SERVER['REMOTE_USER'] değişkeni yoksa tüm kullanıcıların kimliği diğer ayarlara göre doğrulanır.",
+ "LdapUserAdded": "LDAP kullanıcısı eklendi!",
+ "LdapFunctionsMissing": "Bu uygulama ekinin çalışması için gerekli olan PHP LDAP eklentisi etkinleştirilmemiş gibi görünüyor. Lütfen bu eklentiyi kurun.",
+ "CannotConnectToServer": "LDAP sunucusu ile bağlantı kurulamadı.",
+ "CannotConnectToServers": "%s LDAP sunucularınızdan hiç biri ile bağlantı kurulamadı.",
+ "LDAPServers": "LDAP Sunucuları",
+ "OneUser": "1 kullanıcı",
+ "MemberOfCount": "Bu grubun üyesi olan %s kayıt var.",
+ "FilterCount": "Bu süzgece uyan %s kayıt var.",
+ "Test": "Sına",
+ "InvalidFilter": "Süzgeç geçersiz.",
+ "ServerName": "Sunucu adı",
+ "FirstNameField": "Ad Alanı",
+ "FirstNameFieldDescription": "Kullanıcının adının bulunduğu LDAP özniteliği.",
+ "LastNameField": "Soyad Alanı",
+ "LastNameFieldDescription": "Kullanıcının soyadının bulunduğu LDAP özniteliği.",
+ "FirstLastNameForAlias": "LDAP üzerinde kısaltma bulunamaz ise kısaltmanın oluşturulması için kullanılacak ad ve soyad öznitelikleri.",
+ "NewUserDefaultSitesViewAccess": "Yeni Kullanıcılara Görme İzni Verilecek Başlangıç Siteleri",
+ "NewUserDefaultSitesViewAccessDescription": "Belirtildiğinde, ilk kez eşitlenen LDAP kullanıcılara bu siteleri görüntüleme izni verilir. Kulanıcı kodlarının listesi ya da 'all' olarak ayarlanmalıdır.",
+ "EnableLdapAccessSynchronization": "Kullanıcı İzinleri LDAP Üzerinden Eşitlensin",
+ "EnableLdapAccessSynchronizationDescription": "Bu seçenek etkinleştirildiğinde, kullanıcı izinleri özel LDAP öznitelikleri tarafından belirlenir. Not: Bu özelliği kullanabilmek için LDAP sunucunuzun şemasını değiştirmeniz gerekir.",
+ "LdapViewAccessField": "LDAP Görüntüleme İzni Alanı",
+ "LdapViewAccessFieldDescription": "Kullanıcının görüntüleme izni olan siteleri belirleyecek LDAP özniteliği.",
+ "LdapAdminAccessField": "LDAP Yönetici İzni Alanı",
+ "LdapAdminAccessFieldDescription": "Kullanıcının yönetme izni olan siteleri belirleyecek LDAP özniteliği.",
+ "LdapSuperUserAccessField": "LDAP Süper Kullanıcı İzni Alanı",
+ "LdapSuperUserAccessFieldDescription": "Kullanıcının Süper Kullanıcı izni olup olmadığını belirleyecek LDAP özniteliği.",
+ "LdapUserAccessAttributeServerSpecDelimiter": "Kullanıcı İzni Özniteliği Sunucu Belirteci Ayıracı",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "Sunucu belirteçlerini ayıtmak için kullanılacak dizge. ';' olarak ayarlanırsa izin özniteliğinin şu şekilde görünmesi beklenir '<server-spec>;<server-spec>;...'.",
+ "LdapUserAccessAttributeServerSeparator": "Kullanıcı İzni Özniteliği Sunucu ve Site Listesi Ayıracı",
+ "LdapUserAccessAttributeServerSeparatorDescription": "Matomo sunucu kopya kodları ile site kodu listelerini ayırmak için kullanılacak dizge. ':' olarak ayarlanırsa izin özniteliğinin şu şekilde görünmesi beklenir '<server-id>:<site-list>;<server-id>:<site-list>'.",
+ "ThisMatomoInstanceName": "Bu Matomo Kopyasının Özel Adı",
+ "ThisMatomoInstanceNameDescription": "Bir erişim özniteliği için bu Matomo kopyasını belirten özel ad. Bir ad belirtilmediğinde bu Matomo kopyasının temel adresi kullanılır.",
+ "UnsupportedPasswordReset": "Kullanıcı parolası sıfırlanamıyor, lütfen bir yönetici ile görüşün.",
+ "UserSyncSettings": "Kullanıcı Eşitleme Ayarları",
+ "AccessSyncSettings": "İzin Eşitleme Ayarları",
+ "SynchronizeUsersAfterLogin": "Kullanıcılar Oturum Açıldıktan Sonra Eşitlensin",
+ "SynchronizeUsersAfterLoginDescription": "Bu seçenek etkinleştirildiğinde, her oturum açıldığında kullanıcı bilgileri LDAP üzerinden eşitlenir.",
+ "SynchronizeUsersAfterLoginDescription2": "Bu seçenek devre dışı bırakıldığında, LDAP kimlik doğrulaması etkinleştirilmemiş ise oturum açıldıktan sonra kullanıcı bilgileri eşitlenmez. Olabildiğinde LDAP bağlantılarından tümüyle kaçınılır.",
+ "UseLdapForAuthentication": "Kimlik Doğrulaması için Her Zaman LDAP Kullanılsın",
+ "UseLdapForAuthenticationDescription": "Bu seçenek devre dışı bırakıldığında, kullanıcı parolaları LDAP sunucusunun yanında Matomo veritabanında da saklanır. Bu yöntem kullanıldığında daha hızlı oturum açılabilir ancak daha az güvenlidir.",
+ "PasswordField": "Kullanıcı Parolası Alanı",
+ "PasswordFieldDescription": "Bir kullanıcın parolasının bulunduğu LDAP özniteliği. Örnek: 'userPassword' ya da 'unicodePwd'.",
+ "PasswordFieldDescription2": "'Kimlik Doğrulaması için Her Zaman LDAP Kullanılsın' seçeneği etkinleştirilmiş ve 'Yeni Kullanıcılar için Rastgele token_auth Üretilsin' seçeneği devre dışı bırakılmış ise, LDAP üzerinde bu alanın değeri kullanıcının karılmış ya da şifrelenmiş parolası olmalıdır.",
+ "ReadMoreAboutAccessSynchronization": "Kullnıcı izinlerinin eşitlenmesi hakkında ayrıntılı bilgi almak için %1$sbelgelere bakın%2$s.",
+ "ExpectedLdapAttributes": "Beklenen LDAP Öznitelikleri",
+ "ExpectedLdapAttributesPrelude": "Bu yapılandırma kullanıldığında, LoginLdap uygulama eki LDAP sunucudan öznitelikleri şu şekilde bekler",
+ "NetworkTimeout": "LDAP Ağ İsteği Zaman Aşımı (Saniye)",
+ "NetworkTimeoutDescription": "Saniye cinsinden LDAP ağ isteği zaman aşımı sınırı.",
+ "NetworkTimeoutDescription2": "Zaman aşımı süresi arttırıldığında, bir ya da bir kaç LDAP sunucusunda sorun olursa LDAP üzerinden oturum açan Matomo kullanıcılarının daha fazla beklemesi gerekir.",
+ "Go": "Git",
+ "LoginPluginEnabledWarning": "Hem %1$s hem de %2$s uygulama ekleri etkinleştirilmiş!. Bu durum LDAP kullanıcılarının oturum açmasını engeller. Lütfen %1$s uygulama ekini devre dışı bırakmak.",
+ "MemberOfField": "LDAP memberOf Alanı",
+ "MemberOfFieldDescription": "Bir kullanıcın parolasının bulunduğu LDAP özniteliği. Örnek: 'memberOf'.",
+ "LdapUrlPortWarning": "Sunucu adı bir LDAP adresi olarak ayarlandığında kapı seçeneği yok sayılır (Örnek: localhost yerine ldap:\/\/localhost\/ gibi).",
+ "UpdateFromPre300Warning": "3.0.0 öncesi bir sürümden güncelleme yaptığınız için bu seçeneği işaretlenmemiş olarak bırakmanız iyi olabilir. 3.0.0 öncesi sürümlerde kullanıcı parolaları hem LDAP hem Matomo veritabanında saklanır. Güncelleme öncesinde eklenmiş olan kullanıcıların oturum açabilmesi için bu seçenek işaretlenmemelidir.",
+ "MobileAppIntegrationNote": "Not: Resmi mobil uygulama ile LDAP kullanmayı planlıyorsanız, bu seçeneği işaretlenmemiş olarak bırakmanız gerekir. Şu andaki durumda parolaları Matomo veritabanında saklanmayan kullanıcılar mobil uygulama üzerinden kimlik doğrulaması yapamaz.",
+ "PasswordFieldHelp": "Var olan parolayı değiştirmek için bir parola yazın ya da parolanın değiştirilmemesi için bu alanı boş bırakın.",
+ "LdapUserCantChangePassword": "LDAP kullanıcıları için parola değiştirme özelliği desteklenmiyor. LDAP kullandığınız için kullanıcı ayarlarınız doğrudan LDAP tarafından belirleniyor. Ayrıntılı bilgi almak için LDAP sunucusu yöneticisi ya da Matomo yöneticisi ile görüşün."
+ }
+} \ No newline at end of file
diff --git a/lang/uk.json b/lang/uk.json
new file mode 100644
index 0000000..1397031
--- /dev/null
+++ b/lang/uk.json
@@ -0,0 +1,5 @@
+{
+ "LoginLdap": {
+ "Test": "Тест"
+ }
+} \ No newline at end of file
diff --git a/lang/vi.json b/lang/vi.json
new file mode 100644
index 0000000..6d2646a
--- /dev/null
+++ b/lang/vi.json
@@ -0,0 +1,6 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "Plugin Đăng nhập với xác thực LDAP. Là một sự thay thế cho kiểu đăng nhập tiêu chuẩn cũ của Matomo.",
+ "Test": "Thử nghiệm"
+ }
+} \ No newline at end of file
diff --git a/lang/zh-cn.json b/lang/zh-cn.json
new file mode 100644
index 0000000..feecc26
--- /dev/null
+++ b/lang/zh-cn.json
@@ -0,0 +1,13 @@
+{
+ "LoginLdap": {
+ "LdapPort": "服务器端口",
+ "AdminPass": "LDAP 密码",
+ "Filter": "LDAP 搜索条件",
+ "FilterCount": "%s 符合这个条件。",
+ "Test": "测试",
+ "FirstNameField": "名字字段",
+ "FirstNameFieldDescription": "LDAP 属性包含用户的名字",
+ "ExpectedLdapAttributes": "需要 LDAP 属性",
+ "Go": "下一步"
+ }
+} \ No newline at end of file
diff --git a/lang/zh-tw.json b/lang/zh-tw.json
new file mode 100644
index 0000000..05441a6
--- /dev/null
+++ b/lang/zh-tw.json
@@ -0,0 +1,91 @@
+{
+ "LoginLdap": {
+ "PluginDescription": "以 LDAP 驗證來登入的外掛。提供提供標準 Matomo 登入的替代方案。",
+ "MenuLdap": "LDAP 使用者",
+ "LoadUser": "從 LDAP 讀取使用者",
+ "LoadUserDescription": "你可以使用這個表單來同步 LDAP 中的使用者。",
+ "LoadUserCommandDesc": "要同步更多使用者,使用 %s 指令。",
+ "Settings": "LDAP 設定",
+ "AliasField": "使用者別名欄位",
+ "BaseDn": "專有名稱(DN)",
+ "LdapPort": "伺服器端口",
+ "MailField": "Email 欄位",
+ "ServerUrl": "伺服器網址",
+ "UserIdField": "使用者 ID 欄位",
+ "AliasFieldDescription": "用於產生使用者 Matomo 別名的 LDAP 屬性,例如「cn」。",
+ "MailFieldDescription": "包含使用者 Email 的 LDAP 屬性,例如「mail」。",
+ "UserIdFieldDescription": "包含使用者使用者名稱的 LDAP 屬性,例如「uid」,或是使用中的資料夾:「userPrincipalName」。",
+ "UserNotFound": "找不到使用者「%s」!",
+ "NoUserName": "沒有提供使用者名稱!",
+ "UsernameSuffix": "Email 後綴",
+ "UsernameSuffixDescription": "附加到使用者名稱後方來為沒有 Email 的 LDAP 使用者產生 Email。例如設定為「@localhost.com」。",
+ "AdminUser": "LDAP 綁定使用者名稱",
+ "AdminUserDescription": "有權限存取其他使用者條目的使用者,或是留空綁定匿名。",
+ "AdminPass": "LDAP 密碼",
+ "MemberOf": "必要的使用者群組",
+ "MemberOfDescription": "使用者要成為成員必要的群組,例如「cn=Matomo User,cn=Users,dc=organization,dc=com」。",
+ "MemberOfDescription2": "注意:這個設定使用 memberOf=? 過濾。部分 LDAP 伺服器需要做其他設定才能運作。",
+ "Filter": "LDAP 搜尋過濾",
+ "FilterDescription": "LDAP 過濾通常用來決定哪個條目要用於驗證,例如「(objectClass=person)」。",
+ "Kerberos": "使用網頁伺服器驗證(例如 Kerberos SSO)",
+ "KerberosDescription": "如果啟用,這個外掛將檢查 $_SERVER['REMOTE_USER'] 變數來揣測使用者是否已驗證。如果 $_SERVER['REMOTE_USER'] 不存在,所有使用者將以其他設定來驗證。",
+ "LdapUserAdded": "LDAP 使用者新增成功!",
+ "LdapFunctionsMissing": "PHP 的 LDAP 擴充功能似乎沒啟用。必須要啟用後才能使用 LoginLdap 外掛,請先安裝它。",
+ "CannotConnectToServer": "無法連接 LDAP 伺服器。",
+ "CannotConnectToServers": "無法連接任一 %s 個 LDAP 伺服器。",
+ "LDAPServers": "LDAP 伺服器",
+ "OneUser": "1 位使用者",
+ "MemberOfCount": "%s 為是這個群組的一員。",
+ "FilterCount": "%s 位符合過濾條件。",
+ "Test": "測試",
+ "InvalidFilter": "無效的過濾。",
+ "ServerName": "伺服器名稱",
+ "FirstNameField": "名字欄位",
+ "FirstNameFieldDescription": "包含使用者名字的 LDAP 屬性。",
+ "LastNameField": "姓氏欄位",
+ "LastNameFieldDescription": "包含使用者的姓氏的 LDAP 屬性。",
+ "FirstLastNameForAlias": "如果在 LDAP 中找不到名字和姓氏,會用它們來產生別名。",
+ "NewUserDefaultSitesViewAccess": "新使用者一開始有檢視存取權限的網站",
+ "NewUserDefaultSitesViewAccessDescription": "如果定義,當 LDAP 使用者首次同步時,他將會被授予這些網站的檢視存取權限。應該設定為 ID 列表或「全部」。",
+ "EnableLdapAccessSynchronization": "啟用 LDAP 使用者存取同步",
+ "EnableLdapAccessSynchronizationDescription": "如果啟用,使用者存取層級由自訂 LDAP 屬性來決定。注意:使用這個功能你需要編輯你 LDAP 伺服器的 schema。",
+ "LdapViewAccessField": "LDAP 檢視存取欄位",
+ "LdapViewAccessFieldDescription": "這個自訂的 LDAP 屬性決定使用者有哪個網站的檢視存取權限。",
+ "LdapAdminAccessField": "LDAP 管理員存取欄位",
+ "LdapAdminAccessFieldDescription": "這個自訂的 LDAP 屬性決定使用者有哪個網站的管理員存取權限。",
+ "LdapSuperUserAccessField": "LDAP 超級使用者存取欄位",
+ "LdapSuperUserAccessFieldDescription": "這個自訂的 LDAP 屬性決定使用者是否為超級使用者。",
+ "LdapUserAccessAttributeServerSpecDelimiter": "使用者存取屬性伺服器格式分隔符",
+ "LdapUserAccessAttributeServerSpecDelimiterDescription": "這個字串用於使用者存取屬性中分隔伺服器格式。如果設定為「;」,存取屬性的格式會像是「<server-spec>;<server-spec>;...」。",
+ "LdapUserAccessAttributeServerSeparator": "使用者存取屬性伺服器和網站列表分隔符",
+ "LdapUserAccessAttributeServerSeparatorDescription": "這個字串用於分隔 Matomo 伺服器事例和網站 ID 列表。 如果設定為「:」,存取屬性的格式會像是「'<server-id>:<site-list>;<server-id>:<site-list>」。",
+ "ThisMatomoInstanceName": "這個 Matomo 事例的特殊名稱",
+ "ThisMatomoInstanceNameDescription": "用來在存取屬性中識別這個 Matomo 事例的特殊名稱。如果沒有定義,我們會用這個 Matomo 的網址。",
+ "UnsupportedPasswordReset": "無法重置這位使用者的密碼,請聯絡管理員。",
+ "UserSyncSettings": "使用者同步設定",
+ "AccessSyncSettings": "存取同步設定",
+ "SynchronizeUsersAfterLogin": "成功登入後同步使用者",
+ "SynchronizeUsersAfterLoginDescription": "如果啟用,LDAP 中的使用者詳細資料將永遠在成功登入後同步。",
+ "SynchronizeUsersAfterLoginDescription2": "如果停用,除非啟用 LDAP 驗證,否則外掛將不會在登入後同步使用者資訊。如果可能,將完全避免 LDAP 連接。",
+ "UseLdapForAuthentication": "永遠使用 LDAP 來驗證",
+ "UseLdapForAuthenticationDescription": "如果停用,除了 LDAP 外的使用者密碼將會儲存在 Matomo 資料庫中。登入速度較快,但安全性較低。",
+ "PasswordField": "使用者密碼欄位",
+ "PasswordFieldDescription": "包含使用者密碼的 LDAP 屬性名稱,例如「userPassword」或「unicodePwd」。",
+ "PasswordFieldDescription2": "如果啟用了「永遠使用 LDAP 來驗證」和停用了「為新使用者產生隨機 token_auth」,這個欄位在 LDAP 中的值必須是使用者混淆過或加密過的密碼。",
+ "ReadMoreAboutAccessSynchronization": "要了解更多關於使用者存取同步,%1$s閱讀我們的說明文件%2$s。",
+ "ExpectedLdapAttributes": "期望的 LDAP 屬性",
+ "ExpectedLdapAttributesPrelude": "在這個設定中,LoginLdap 期望 LDAP 的屬性看起來會像:",
+ "NetworkTimeout": "LDAP 網路請求逾時(秒)",
+ "NetworkTimeoutDescription": "LDAP 網路請求的逾時限制,以秒為單位。",
+ "NetworkTimeoutDescription2": "如果你的伺服器反應越來越慢,設定的逾時越大,你的使用者要透過 LDAP 登入 Matomo 時所等待時間就越多。",
+ "Go": "前往",
+ "LoginPluginEnabledWarning": "%1$s 和 %2$s 外掛已啟用!這將導致 LDAP 使用者登入失敗,請停用 %1$s 外掛。",
+ "MemberOfField": "LDAP memberOf 欄位",
+ "MemberOfFieldDescription": "你要用於 LDAP 標示會員的欄位名稱,預設為「memberOf」。",
+ "LdapUrlPortWarning": "如果伺服器網址設定為 LDAP 網址(如 ldap:\/\/localhost\/ 而不是 localhost),端口選項將會被忽略。",
+ "UpdateFromPre300Warning": "你從 pre-3.0.0 版本升級後,應該保持不勾選這個選項。在版本 3.0.0 之前,使用者密碼被同時儲存在 LDAP 和 Matomo 資料庫中。這個選項必須不勾選,升級之前新增的使用者才能夠登入。",
+ "MobileAppIntegrationNote": "注意:如果你打算在官方手機應用程式中使用 LDAP,你必須保持不勾選這個選項。目前,如果使用者密碼沒儲存在 Matomo 資料庫中,手機應用程式就無法驗證使用者。",
+ "PasswordFieldHelp": "輸入密碼以覆蓋目前的密碼或留空不修改。",
+ "LdapUserCantChangePassword": "無法為 LDAP 使用者變更密碼。當你使用 LDAP,你的使用者設定都要直接在 LDAP 中管理。要取得更多資訊,請聯絡你的 LDAP 伺服器或 Matomo 管理員。"
+ }
+} \ No newline at end of file
diff --git a/plugin.json b/plugin.json
new file mode 100644
index 0000000..3c3b03a
--- /dev/null
+++ b/plugin.json
@@ -0,0 +1,30 @@
+{
+ "name": "LoginLdap",
+ "version": "4.0.4",
+ "description": "LDAP authentication and synchronization for Piwik.",
+ "theme": false,
+ "keywords": ["ldap", "login", "authentication", "active", "directory", "kerberos", "sso"],
+ "license": "GPL v3+",
+ "homepage": "https://github.com/piwik/plugin-LoginLdap",
+ "require": {
+ "piwik": ">=3.0.0-rc1,<4.0.0-b1",
+ "php": ">=5.5"
+ },
+ "authors": [
+ {
+ "name": "Piwik",
+ "email": "hello@piwik.org",
+ "homepage": "https://github.com/piwik"
+ },
+ {
+ "name": "Aivo Koger",
+ "email": "aivo.koger@gmail.com",
+ "homepage": "https://github.com/tehnotronic"
+ },
+ {
+ "name": "Stefan Kreuter",
+ "email": "info@gigatec.de",
+ "homepage": "http://www.gigatec.de"
+ }
+ ]
+}
diff --git a/screenshots/.gitkeep b/screenshots/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/screenshots/.gitkeep
diff --git a/screenshots/LoginLdap_Admin_admin_page.png b/screenshots/LoginLdap_Admin_admin_page.png
new file mode 100644
index 0000000..779f174
--- /dev/null
+++ b/screenshots/LoginLdap_Admin_admin_page.png
Binary files differ
diff --git a/templates/index.twig b/templates/index.twig
new file mode 100644
index 0000000..9c26935
--- /dev/null
+++ b/templates/index.twig
@@ -0,0 +1,300 @@
+{% extends 'admin.twig' %}
+
+{% block content %}
+ {% if isLoginControllerActivated|default(false) %}
+ <div piwik-notification context="warning" noclear="true">
+ <strong>{{ 'General_Warning'|translate }}</strong>: {{ 'LoginLdap_LoginPluginEnabledWarning'|translate("Login", "LoginLdap") }}
+ </div>
+ {% endif %}
+
+<div ng-controller="LoginLdapAdminController as adminController" data-servers="{{ servers|json_encode }}">
+
+ <div piwik-ajax-form
+ submit-api-method="'LoginLdap.saveLdapConfig'"
+ use-custom-data-binding="true"
+ send-json-payload="true">
+
+ <div piwik-content-block id="ldapSettings"
+ content-title="{{ 'LoginLdap_Settings'|translate }}">
+
+
+ {% if updatedFromPre30|default %}
+ <div piwik-notification context="warning" noclear="true" id="pre300AlwaysUseLdapWarning">
+ <strong>{{ 'General_Note'|translate }}</strong>: {{ 'LoginLdap_UpdateFromPre300Warning'|translate }}
+ </div>
+ {% endif %}
+
+ <div piwik-field uicontrol="checkbox" name="synchronize_users_after_login"
+ title="{{ 'LoginLdap_UseLdapForAuthentication'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.use_ldap_for_authentication }}"
+ ng-model="ajaxForm.data.use_ldap_for_authentication"
+ inline-help="{{ 'LoginLdap_UseLdapForAuthenticationDescription'|translate|e('html_attr') }}<br /><br />{{ 'LoginLdap_MobileAppIntegrationNote'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="checkbox" name="use_webserver_auth"
+ title="{{ 'LoginLdap_Kerberos'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.use_webserver_auth }}"
+ ng-model="ajaxForm.data.use_webserver_auth"
+ inline-help="{{ 'LoginLdap_KerberosDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_network_timeout"
+ title="{{ 'LoginLdap_NetworkTimeout'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_network_timeout }}"
+ ng-model="ajaxForm.data.ldap_network_timeout"
+ inline-help="{{ 'LoginLdap_NetworkTimeoutDescription'|translate|e('html_attr') }}<br />{{ 'LoginLdap_NetworkTimeoutDescription2'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="required_member_of_field"
+ title="{{ 'LoginLdap_MemberOfField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.required_member_of_field }}"
+ ng-model="ajaxForm.data.required_member_of_field"
+ inline-help="{{ 'LoginLdap_MemberOfFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-login-ldap-testable-field uicontrol="text" name="required_member_of"
+ title="{{ 'LoginLdap_MemberOf'|translate|e('html_attr') }}"
+ inline-help="{{ 'LoginLdap_MemberOfDescription'|translate|e('html_attr') }} <br />{{ 'LoginLdap_MemberOfDescription2'|translate|e('html_attr') }}"
+ ng-model="ajaxForm.data.required_member_of"
+ value="{{ ldapConfig.required_member_of }}"
+ test-api-method="'LoginLdap.getCountOfUsersMemberOf'"
+ test-api-method-arg="'memberOf'"
+ success-translation="LoginLdap_MemberOfCount">
+ </div>
+
+ <div piwik-login-ldap-testable-field uicontrol="text" name="ldap_user_filter"
+ title="{{ 'LoginLdap_Filter'|translate|e('html_attr') }}"
+ inline-help="{{ 'LoginLdap_FilterDescription'|translate|e('html_attr') }}"
+ ng-model="ajaxForm.data.ldap_user_filter"
+ value="{{ ldapConfig.ldap_user_filter }}"
+ test-api-method="'LoginLdap.getCountOfUsersMatchingFilter'"
+ test-api-method-arg="'filter'"
+ success-translation="LoginLdap_FilterCount">
+ </div>
+
+ <hr />
+
+ <div piwik-save-button saving="ajaxform.isSubmitting" onconfirm="ajaxForm.submitForm()"></div>
+ </div>
+
+ <div piwik-content-block id="ldapUserMappingSettings"
+ content-title="{{ 'LoginLdap_UserSyncSettings'|translate }}">
+
+ <div piwik-field uicontrol="text" name="ldap_user_id_field"
+ title="{{ 'LoginLdap_UserIdField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_user_id_field }}"
+ ng-model="ajaxForm.data.ldap_user_id_field"
+ inline-help="{{ 'LoginLdap_UserIdFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_password_field"
+ title="{{ 'LoginLdap_PasswordField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_password_field }}"
+ ng-model="ajaxForm.data.ldap_password_field"
+ inline-help="{{ 'LoginLdap_PasswordFieldDescription'|translate|e('html_attr') }}<br /><br />{{ 'LoginLdap_PasswordFieldDescription2'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_mail_field"
+ title="{{ 'LoginLdap_MailField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_mail_field }}"
+ ng-model="ajaxForm.data.ldap_mail_field"
+ inline-help="{{ 'LoginLdap_MailFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_alias_field"
+ title="{{ 'LoginLdap_AliasField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_alias_field }}"
+ ng-model="ajaxForm.data.ldap_alias_field"
+ inline-help="{{ 'LoginLdap_AliasFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_first_name_field"
+ title="{{ 'LoginLdap_FirstNameField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_first_name_field }}"
+ ng-model="ajaxForm.data.ldap_first_name_field"
+ inline-help="{{ 'LoginLdap_FirstNameFieldDescription'|translate|e('html_attr') }}<br /><br />{{ 'LoginLdap_FirstLastNameForAlias'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_last_name_field"
+ title="{{ 'LoginLdap_LastNameField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_last_name_field }}"
+ ng-model="ajaxForm.data.ldap_last_name_field"
+ inline-help="{{ 'LoginLdap_LastNameFieldDescription'|translate|e('html_attr') }}<br /><br />{{ 'LoginLdap_FirstLastNameForAlias'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="user_email_suffix"
+ title="{{ 'LoginLdap_UsernameSuffix'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.user_email_suffix }}"
+ ng-model="ajaxForm.data.user_email_suffix"
+ inline-help="{{ 'LoginLdap_UsernameSuffixDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="new_user_default_sites_view_access"
+ title="{{ 'LoginLdap_NewUserDefaultSitesViewAccess'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.new_user_default_sites_view_access }}"
+ ng-model="ajaxForm.data.new_user_default_sites_view_access"
+ inline-help="{{ 'LoginLdap_NewUserDefaultSitesViewAccessDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <hr />
+
+ <div piwik-save-button saving="ajaxform.isSubmitting" onconfirm="ajaxForm.submitForm()"></div>
+
+ </div>
+
+ <div piwik-content-block id="ldapUserMappingSettings"
+ content-title="{{ 'LoginLdap_AccessSyncSettings'|translate }}">
+
+ <p>
+ {{ 'LoginLdap_ReadMoreAboutAccessSynchronization'|translate('<a target="_blank" href="https://github.com/piwik/plugin-LoginLdap#piwik-access-synchronization">','</a>')|raw }}
+ </p>
+
+ <div piwik-field uicontrol="checkbox" name="enable_synchronize_access_from_ldap"
+ title="{{ 'LoginLdap_EnableLdapAccessSynchronization'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.enable_synchronize_access_from_ldap }}"
+ ng-model="ajaxForm.data.enable_synchronize_access_from_ldap"
+ inline-help="{{ 'LoginLdap_EnableLdapAccessSynchronizationDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div ng-show="ajaxForm.data.enable_synchronize_access_from_ldap">
+ <div piwik-notification context="info">
+ <strong>{{ 'LoginLdap_ExpectedLdapAttributes'|translate }}</strong><br/>
+ <br/>
+ {{ 'LoginLdap_ExpectedLdapAttributesPrelude'|translate }}:<br/>
+ <br/>
+ <ul>
+ <li>{% verbatim %}{{ $parent.$parent.$parent.$parent.$parent.getSampleViewAttribute(ajaxForm.data) }}{% endverbatim %}</li>
+ <li>{% verbatim %}{{ $parent.$parent.$parent.$parent.$parent.getSampleAdminAttribute(ajaxForm.data) }}{% endverbatim %}</li>
+ <li>{% verbatim %}{{ $parent.$parent.$parent.$parent.$parent.getSampleSuperuserAttribute(ajaxForm.data) }}{% endverbatim %}</li>
+ </ul>
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_view_access_field"
+ title="{{ 'LoginLdap_LdapViewAccessField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_view_access_field }}"
+ ng-model="ajaxForm.data.ldap_view_access_field"
+ inline-help="{{ 'LoginLdap_LdapViewAccessFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_admin_access_field"
+ title="{{ 'LoginLdap_LdapAdminAccessField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_admin_access_field }}"
+ ng-model="ajaxForm.data.ldap_admin_access_field"
+ inline-help="{{ 'LoginLdap_LdapAdminAccessFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="ldap_superuser_access_field"
+ title="{{ 'LoginLdap_LdapSuperUserAccessField'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.ldap_superuser_access_field }}"
+ ng-model="ajaxForm.data.ldap_superuser_access_field"
+ inline-help="{{ 'LoginLdap_LdapSuperUserAccessFieldDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="user_access_attribute_server_specification_delimiter"
+ title="{{ 'LoginLdap_LdapUserAccessAttributeServerSpecDelimiter'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.user_access_attribute_server_specification_delimiter }}"
+ ng-model="ajaxForm.data.user_access_attribute_server_specification_delimiter"
+ inline-help="{{ 'LoginLdap_LdapUserAccessAttributeServerSpecDelimiterDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="user_access_attribute_server_separator"
+ title="{{ 'LoginLdap_LdapUserAccessAttributeServerSeparator'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.user_access_attribute_server_separator }}"
+ ng-model="ajaxForm.data.user_access_attribute_server_separator"
+ inline-help="{{ 'LoginLdap_LdapUserAccessAttributeServerSeparatorDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text" name="instance_name"
+ title="{{ 'LoginLdap_ThisPiwikInstanceName'|translate|e('html_attr') }}"
+ value="{{ ldapConfig.instance_name }}"
+ ng-model="ajaxForm.data.instance_name"
+ inline-help="{{ 'LoginLdap_ThisPiwikInstanceNameDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <hr />
+
+ <div piwik-save-button saving="ajaxform.isSubmitting" onconfirm="ajaxForm.submitForm()"></div>
+ </div>
+ </div>
+
+ </div>
+
+ <div piwik-content-block
+ id="ldapUserMappingSettings"
+ content-title="{{ 'LoginLdap_LoadUser'|translate|e('html_attr') }}">
+
+ <p>{{ 'LoginLdap_LoadUserDescription'|translate }}</p>
+
+ <div piwik-field uicontrol="text"
+ ng-model="userToSynchronize" placeholder="Enter a username...">
+ </div>
+
+ <div piwik-save-button onconfirm="$parent.$parent.synchronizeUser(userToSynchronize)" value="{{ 'LoginLdap_Go'|translate }}"></div>
+
+ <img src="plugins/Morpheus/images/loading-blue.gif"
+ ng-show="$parent.$parent.currentSynchronizeUserRequest"/><br/>
+ <br/>
+ <div ng-show="$parent.$parent.synchronizeUserError || $parent.$parent.synchronizeUserDone">
+ <div ng-if="$parent.$parent.synchronizeUserError">{% verbatim %}{{ $parent.$parent.$parent.synchronizeUserError }}{% endverbatim %}</div>
+ <div ng-if="$parent.$parent.synchronizeUserDone">{% verbatim %}<strong>{{ 'General_Done'|translate }}!</strong>{% endverbatim %}</div>
+ <br/>
+ </div>
+
+ <span>{{ 'LoginLdap_LoadUserCommandDesc'|translate('<a target="_blank" href="https://github.com/piwik/plugin-LoginLdap#commands">loginldap:synchronize-users</a>')|raw }}</span>
+
+ </div>
+
+ <div piwik-content-block
+ content-title="{{ 'LoginLdap_LDAPServers'|translate|e('html_attr') }}">
+
+ <div piwik-ajax-form
+ ng-model="servers"
+ submit-api-method="'LoginLdap.saveServersInfo'"
+ send-json-payload="true"
+ use-custom-data-binding="true">
+ <div ng-repeat="serverInfo in ajaxForm.data" id="ldapServersTable">
+ <div piwik-field uicontrol="text"
+ title="{{ 'LoginLdap_ServerName'|translate|e('html_attr') }}"
+ ng-model="serverInfo.name">
+ </div>
+
+ <div piwik-field uicontrol="text"
+ title="{{ 'LoginLdap_ServerUrl'|translate|e('html_attr') }}"
+ ng-model="serverInfo.hostname" placeholder="localhost">
+ </div>
+
+ <div piwik-field uicontrol="text"
+ title="{{ 'LoginLdap_LdapPort'|translate|e('html_attr') }}"
+ ng-model="serverInfo.port" placeholder="389"
+ inline-help="{{ 'LoginLdap_LdapUrlPortWarning'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="text"
+ title="{{ 'LoginLdap_BaseDn'|translate|e('html_attr') }}"
+ ng-model="serverInfo.base_dn" placeholder="dc=example,dc=site,dc=org">
+ </div>
+
+ <div piwik-field uicontrol="text"
+ title="{{ 'LoginLdap_AdminUser'|translate|e('html_attr') }}"
+ ng-model="serverInfo.admin_user" placeholder="cn=admin,dc=example,dc=site,dc=org"
+ inline-help="{{ 'LoginLdap_AdminUserDescription'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="password"
+ title="{{ 'LoginLdap_AdminPass'|translate|e('html_attr') }}"
+ ng-model="serverInfo.admin_pass"
+ inline-help="{{ 'LoginLdap_PasswordFieldHelp'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-save-button onconfirm="ajaxForm.data.splice($index, 1)" value="{{ 'General_Delete'|translate }}"></div>
+
+ </div>
+
+ <hr />
+
+ <div piwik-save-button onconfirm="ajaxForm.data.addServer()" value="{{ 'General_Add'|translate }}"></div>
+ <div piwik-save-button saving="ajaxform.isSubmitting" onconfirm="ajaxForm.submitForm()"></div>
+ </div>
+ </div>
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/tests/Integration/ApiTest.php b/tests/Integration/ApiTest.php
new file mode 100644
index 0000000..5ba867e
--- /dev/null
+++ b/tests/Integration/ApiTest.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Exception;
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\API;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_ApiTest
+ */
+class ApiTest extends LdapIntegrationTest
+{
+ /**
+ * @var API
+ */
+ private $api;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->api = new API();
+ }
+
+ public function test_getCountOfUsersMemberOf_ReturnsZero_WhenNoUsersAreMemberOfGroup()
+ {
+ $count = $this->api->getCountOfUsersMemberOf("not()hing");
+ $this->assertEquals(0, $count);
+ }
+
+ public function test_getCountOfUsersMemberOf_ReturnsCorrectResponse_WhenUsersAreMemberOfGroup()
+ {
+ $count = $this->api->getCountOfUsersMemberOf("cn=avengers," . self::SERVER_BASE_DN);
+ $this->assertEquals(4, $count);
+ }
+
+ public function test_getCountOfUsersMatchingFilter_ReturnsZero_WhenNoUsersMatchTheFilter()
+ {
+ $count = $this->api->getCountOfUsersMemberOf("(objectClass=whatever)");
+ $this->assertEquals(0, $count);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage LoginLdap_InvalidFilter
+ */
+ public function test_getCountOfUsersMatchingFilter_Throws_WhenFilterIsInvalid()
+ {
+ $this->api->getCountOfUsersMatchingFilter("lksjdf()a;sk");
+ }
+
+ public function test_getCountOfUsersMatchingFilter_ReturnsCorrectResult_WhenUsersMatchFilter()
+ {
+ $count = $this->api->getCountOfUsersMatchingFilter("(objectClass=person)");
+ $this->assertEquals(6, $count);
+ }
+
+ public function test_saveLdapConfig_SavesConfigToINIFile_AndIgnoresInvalidConfigNames()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ Config::getInstance()->LoginLdap['servers'] = array();
+
+ $configToSave = array(
+ 'use_ldap_for_authentication' => 0,
+ 'synchronize_users_after_login' => 0,
+ 'enable_synchronize_access_from_ldap' => 1,
+ 'new_user_default_sites_view_access' => '10,11,13',
+ 'servers' => 'abc',
+ 'nonconfigoption' => 'def'
+ );
+
+ $this->api->saveLdapConfig(json_encode($configToSave));
+
+ $ldapConfig = Config::getInstance()->LoginLdap;
+ $this->assertEquals(0, $ldapConfig['use_ldap_for_authentication']);
+ $this->assertEquals(0, $ldapConfig['synchronize_users_after_login']);
+ $this->assertEquals(1, $ldapConfig['enable_synchronize_access_from_ldap']);
+ $this->assertEquals('10,11,13', $ldapConfig['new_user_default_sites_view_access']);
+ $this->assertTrue(empty($ldapConfig['servers']));
+ $this->assertTrue(empty($ldapConfig['nonconfigoption']));
+ }
+
+ public function test_saveServersInfo_SavesConfigToINIFile_AndIgnoresInvalidServerInfo()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ $serverInfos = array(
+ array(
+ 'name' => 'server1',
+ 'hostname' => 'ahost.com',
+ 'port' => 389,
+ 'base_dn' => 'somedn'
+ ),
+ array(
+ 'invaliddata' => 'sdfjklsdj',
+ 'name' => 'server2',
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'pass'
+ ),
+ );
+
+ $this->api->saveServersInfo(json_encode($serverInfos));
+
+ $this->assertEquals(array(
+ 'hostname' => 'ahost.com',
+ 'port' => 389,
+ 'base_dn' => 'somedn',
+ 'admin_user' => null,
+ 'admin_pass' => null
+ ), Config::getInstance()->LoginLdap_server1);
+
+ $this->assertEquals(array(
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'pass'
+ ), Config::getInstance()->LoginLdap_server2);
+ }
+
+ public function test_saveServersInfo_DoesNotOverwritePassword_IfPasswordFieldBlank()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ Config::getInstance()->LoginLdap_server2 = array(
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'firstpass'
+ );
+
+ $serverInfos = array(
+ array(
+ 'name' => 'server2',
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => ''
+ ),
+ );
+
+ $this->api->saveServersInfo(json_encode($serverInfos));
+
+ $this->assertEquals(array(
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'firstpass'
+ ), Config::getInstance()->LoginLdap_server2);
+ }
+
+ public function test_saveServersInfo_OverwritesPassword_IfPasswordFieldNotBlank()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ Config::getInstance()->LoginLdap_server2 = array(
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'firstpass'
+ );
+
+ $serverInfos = array(
+ array(
+ 'name' => 'server2',
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'pass'
+ ),
+ );
+
+ $this->api->saveServersInfo(json_encode($serverInfos));
+
+ $this->assertEquals(array(
+ 'hostname' => 'thehost.com',
+ 'port' => 456,
+ 'base_dn' => 'thedn',
+ 'admin_user' => 'admin',
+ 'admin_pass' => 'pass'
+ ), Config::getInstance()->LoginLdap_server2);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage LoginLdap_UserNotFound
+ */
+ public function test_synchronizeUser_Throws_WhenLdapUserDoesNotExist()
+ {
+ $this->api->synchronizeUser('unknownuser');
+ }
+
+ public function test_synchronizeUser_Succeeds_WhenLdapUserExistsAndIsValid()
+ {
+ $this->api->synchronizeUser(self::TEST_LOGIN);
+ $this->assertStarkSynchronized();
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/AuthenticationTest.php b/tests/Integration/AuthenticationTest.php
new file mode 100644
index 0000000..53e6663
--- /dev/null
+++ b/tests/Integration/AuthenticationTest.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\AuthResult;
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\Db;
+use Piwik\Plugins\LoginLdap\Auth\LdapAuth;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_AuthenticationTest
+ */
+class AuthenticationTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ // test superusers should not have {LDAP} password to test that superusers can
+ // login, even if they are not in LDAP
+ $this->addPreexistingSuperUser();
+
+ $this->addNonLdapUsers();
+ }
+
+ public function test_LdapAuth_AuthenticatesUser_WithCorrectCredentials()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_DoesNotAuthenticateUser_WithIncorrectPassword()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword('slkdjfsd');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_DoesNotAuthenticateUser_WithNonexistantUser()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin('skldfjsd');
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_ChecksMemberOf_WhenAuthenticating()
+ {
+ Config::getInstance()->LoginLdap['memberOf'] = "cn=S.H.I.E.L.D.," . self::SERVER_BASE_DN;
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+
+ Config::getInstance()->LoginLdap['memberOf'] = "cn=avengers," . self::SERVER_BASE_DN;
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_UsesConfiguredFilter_WhenAuthenticating()
+ {
+ Config::getInstance()->LoginLdap['filter'] = "(!(mobile=none))";
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::OTHER_TEST_LOGIN);
+ $ldapAuth->setPassword(self::OTHER_TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_ReturnsCorrectCode_WhenAuthenticatingSuperUsers()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_SUPERUSER_LOGIN);
+ $ldapAuth->setPassword(self::TEST_SUPERUSER_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_ReturnsCorrectCode_WhenAuthenticatingNonLdapSuperUsers()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::NON_LDAP_USER);
+ $ldapAuth->setPassword(self::NON_LDAP_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setTokenAuth($this->getNonLdapUserTokenAuth());
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(self::NON_LDAP_USER, $authResult->getIdentity());
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_ReturnsCorrectCode_WhenAuthenticatingNonLdapNormalUsers()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::NON_LDAP_NORMAL_USER);
+ $ldapAuth->setPassword(self::NON_LDAP_NORMAL_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS, $authResult->getCode());
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setTokenAuth($this->getNonLdapNormalUserTokenAuth());
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(self::NON_LDAP_NORMAL_USER, $authResult->getIdentity());
+ $this->assertEquals(AuthResult::SUCCESS, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_AuthenticatesSuccessfully_WhenTokenAuthOnlyAuthenticationUsed()
+ {
+ $this->test_LdapAuth_AuthenticatesUser_WithCorrectCredentials();
+
+ $tokenAuth = Db::fetchOne("SELECT token_auth FROM " . Common::prefixTable("user") . " WHERE login = ?", array(self::TEST_LOGIN));
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setTokenAuth($tokenAuth);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_AuthenticatesSuccessfully_WhenAuthenticatingNormalPiwikSuperUser()
+ {
+ UsersManagerAPI::getInstance()->addUser('zola', 'hydra___', 'zola@shield.org', $alias = false);
+ UsersManagerAPI::getInstance()->setSuperUserAccess('zola', true);
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin('zola');
+ $ldapAuth->setPassword('hydra___');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_AuthenticatesSuccessfully_WhenAuthenticatingNormalPiwikNonSuperUser()
+ {
+ UsersManagerAPI::getInstance()->addUser('pcoulson', 'thispasswordwillbechanged', 'pcoulson@shield.org', $alias = false);
+ UsersManagerAPI::getInstance()->updateUser('pcoulson', 'vintage');
+
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin('pcoulson');
+ $ldapAuth->setPassword('vintage');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_DoesNotAuthenticate_WhenEmptyLoginProvided()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin('');
+ $ldapAuth->setPassword('lsakjdfdslj');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_LdapAuth_DoesNotAuthenticate_WhenAnonymousLoginProvided()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin('anonymous');
+ $ldapAuth->setPassword('lsakjdfdslj');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ private function getNonLdapUserTokenAuth()
+ {
+ return UsersManagerAPI::getInstance()->getTokenAuth(self::NON_LDAP_USER, md5(self::NON_LDAP_PASS));
+ }
+
+ private function getNonLdapNormalUserTokenAuth()
+ {
+ return UsersManagerAPI::getInstance()->getTokenAuth(self::NON_LDAP_NORMAL_USER, md5(self::NON_LDAP_NORMAL_PASS));
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/BackwardsCompatibilityTest.php b/tests/Integration/BackwardsCompatibilityTest.php
new file mode 100644
index 0000000..870541c
--- /dev/null
+++ b/tests/Integration/BackwardsCompatibilityTest.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\Auth\LdapAuth;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_BackwardsCompatibilityTest
+ */
+class BackwardsCompatibilityTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap = array(
+ 'serverUrl' => @Config::getInstance()->LoginLdap_testserver['hostname'] ?: self::SERVER_HOST_NAME,
+ 'ldapPort' => @Config::getInstance()->LoginLdap_testserver['port'] ?: self::SERVER_PORT,
+ 'baseDn' => self::SERVER_BASE_DN,
+ 'adminUser' => 'cn=fury,' . self::SERVER_BASE_DN,
+ 'adminPass' => 'secrets',
+ 'useKerberos' => 'false'
+ );
+
+ UsersManagerAPI::getInstance()->addUser(self::TEST_LOGIN, self::TEST_PASS, 'billionairephilanthropistplayboy@starkindustries.com', $alias = false);
+ }
+
+ public function testAuthenticationWithOldServerConfig()
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/Commands/SynchronizeUsersTest.php b/tests/Integration/Commands/SynchronizeUsersTest.php
new file mode 100644
index 0000000..338d9f6
--- /dev/null
+++ b/tests/Integration/Commands/SynchronizeUsersTest.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration\Commands;
+
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\Console;
+use Piwik\Db;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\LoginLdap\tests\Integration\LdapIntegrationTest;
+use Symfony\Component\Console\Tester\ApplicationTester;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_SynchronizeUsersTest
+ */
+class SynchronizeUsersTest extends LdapIntegrationTest
+{
+ /**
+ * @var ApplicationTester
+ */
+ protected $applicationTester = null;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $plugins = Config::getInstance()->Plugins;
+ $plugins['Plugins'][] = 'LoginLdap';
+ Config::getInstance()->Plugins = $plugins;
+
+ $application = new Console(self::$fixture->piwikEnvironment);
+ $application->setAutoExit(false);
+
+ $this->applicationTester = new ApplicationTester($application);
+ }
+
+ protected function getCommandDisplayOutputErrorMessage()
+ {
+ return "Command did not behave as expected. Command output: " . $this->applicationTester->getDisplay();
+ }
+
+ public function test_CommandSynchronizesAllUsers_WhenLoginNotSpecified()
+ {
+ $result = $this->applicationTester->run(array(
+ 'command' => 'loginldap:synchronize-users',
+ '-v' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $users = $this->getLdapUserLogins();
+ $this->assertEquals(array('blackwidow', 'captainamerica', 'ironman', 'msmarvel', 'rogue@xmansion.org', 'thor'), $users);
+ }
+
+ public function test_CommandSynchronizesOneUser_WhenLoginSpecified()
+ {
+ $result = $this->applicationTester->run(array(
+ 'command' => 'loginldap:synchronize-users',
+ '--login' => array('ironman'),
+ '-v' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $users = $this->getLdapUserLogins();
+ $this->assertEquals(array('ironman'), $users);
+ }
+
+ public function test_CommandSynchronizesMultipleUsers_WhenMultipleLoginsSpecified()
+ {
+ $result = $this->applicationTester->run(array(
+ 'command' => 'loginldap:synchronize-users',
+ '--login' => array('ironman', 'blackwidow'),
+ '-v' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $users = $this->getLdapUserLogins();
+ $this->assertEquals(array('blackwidow', 'ironman'), $users);
+ }
+
+ public function test_CommandReportsUsersThatAreNotSynchronized_WhenUserMissing_AndUserInfoBrokenInLdap()
+ {
+ $this->markTestSkipped("Can't find a way to inject broken data into result of Ldap\\Client::fetchAll. Waiting for DI.");
+
+ $result = $this->applicationTester->run(array(
+ 'command' => 'loginldap:synchronize-users',
+ '--login' => array('ironman', 'blackwidow', 'missinguser', 'msmarvel'),
+ '-v' => true
+ ));
+
+ $this->assertEquals(2, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $users = $this->getLdapUserLogins();
+ $this->assertEquals(array('blackwidow', 'ironman'), $users);
+
+ $this->assertRegExp("/^.*missinguser.*User.*not found.*$/", $this->applicationTester->getDisplay());
+ $this->assertRegExp("/^.*msmarvel.*LDAP entity missing required.*$/", $this->applicationTester->getDisplay());
+ }
+
+ public function test_CommandSkipsExisitingUsers_IfSkipExistingOptionUsed()
+ {
+ $this->addPreexistingSuperUser();
+
+ $result = $this->applicationTester->run(array(
+ 'command' => 'loginldap:synchronize-users',
+ '--login' => array('ironman', 'captainamerica'),
+ '--skip-existing' => true,
+ '-v' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+ $this->assertContains("Skipping 'captainamerica', already exists in Piwik...", $this->applicationTester->getDisplay());
+
+ $users = $this->getLdapUserLogins();
+ $this->assertEquals(array('ironman'), $users);
+ }
+
+ private function getLdapUserLogins()
+ {
+ $rows = Db::fetchAll("SELECT login from " . Common::prefixTable('user') . ' ORDER BY login');
+ $userMapper = new UserMapper();
+
+ $result = array();
+ foreach ($rows as $row) {
+ if ($userMapper->isUserLdapUser($row['login'])) {
+ $result[] = $row['login'];
+ }
+ }
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/LdapIntegrationTest.php b/tests/Integration/LdapIntegrationTest.php
new file mode 100644
index 0000000..bab0270
--- /dev/null
+++ b/tests/Integration/LdapIntegrationTest.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\Auth\Password;
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\Ldap\LdapFunctions;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+require_once PIWIK_INCLUDE_PATH . '/plugins/LoginLdap/tests/Mocks/LdapFunctions.php';
+
+abstract class LdapIntegrationTest extends IntegrationTestCase
+{
+ const SERVER_HOST_NAME = 'localhost';
+ const SERVER_PORT = 389;
+ const SERVER_BASE_DN = "dc=avengers,dc=shield,dc=org";
+ const GROUP_NAME = 'cn=avengers,dc=avengers,dc=shield,dc=org';
+
+ const TEST_LOGIN = 'ironman';
+ const TEST_PASS = 'piedpiper';
+ const TEST_PASS_LDAP = '{MD5}Dv6yiT/W4FvaM5gBdqHwlQ==';
+
+ const OTHER_TEST_LOGIN = 'blackwidow';
+ const OTHER_TEST_PASS = 'redledger';
+
+ const TEST_SUPERUSER_LOGIN = 'captainamerica';
+ const TEST_SUPERUSER_PASS = 'thaifood';
+
+ const NON_LDAP_USER = 'stan';
+ const NON_LDAP_PASS = 'whereisthefourthwall?';
+
+ const NON_LDAP_NORMAL_USER = 'amber';
+ const NON_LDAP_NORMAL_PASS = 'crossingthefourthwall';
+
+ public function setUp()
+ {
+ if (empty(getenv('PLUGIN_NAME'))) {
+ $this->markTestSkipped('LDAP tests can only be run as plugin tests.');
+ return;
+ }
+
+ if (!function_exists('ldap_bind')) {
+ throw new \Exception("PHP not compiled w/ --with-ldap!");
+ }
+
+ if (!$this->isLdapServerRunning()) {
+ throw new \Exception("LDAP server not found on port localhost:389. For integration tests, an LDAP server must be running with the "
+ . "data and configuration found in tests/travis/setup_ldap.sh script. An OpenLDAP server is expected, but any "
+ . "will work assuming the attributes names & data are the same.");
+ }
+
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap = Config::getInstance()->LoginLdapTest + array(
+ 'servers' => 'testserver',
+ 'use_webserver_auth' => 'false',
+ 'new_user_default_sites_view_access' => '1,2',
+ 'synchronize_users_after_login' => 1
+ );
+
+ Config::getInstance()->LoginLdap_testserver = Config::getInstance()->LoginLdap_testserver + array(
+ 'hostname' => self::SERVER_HOST_NAME,
+ 'port' => self::SERVER_PORT,
+ 'base_dn' => self::SERVER_BASE_DN,
+ 'admin_user' => 'cn=fury,' . self::SERVER_BASE_DN,
+ 'admin_pass' => 'secrets'
+ );
+
+ LdapFunctions::$phpUnitMock = null;
+
+ // create sites referenced in setup_ldap.sh
+ Fixture::createWebsite('2013-01-01 00:00:00');
+ Fixture::createWebsite('2013-01-01 00:00:00');
+ Fixture::createWebsite('2013-01-01 00:00:00');
+ }
+
+ protected function addPreexistingSuperUser()
+ {
+ UsersManagerAPI::getInstance()->addUser(self::TEST_SUPERUSER_LOGIN, self::TEST_SUPERUSER_PASS, 'srodgers@aol.com', $alias = false);
+ UsersManagerAPI::getInstance()->setSuperUserAccess(self::TEST_SUPERUSER_LOGIN, true);
+ }
+
+ protected function addNonLdapUsers()
+ {
+ UsersManagerAPI::getInstance()->addUser(self::NON_LDAP_USER, self::NON_LDAP_PASS, 'whatever@aol.com', $alias = false);
+ UsersManagerAPI::getInstance()->setSuperUserAccess(self::NON_LDAP_USER, true);
+ UsersManagerAPI::getInstance()->addUser(self::NON_LDAP_NORMAL_USER, self::NON_LDAP_NORMAL_PASS, 'witchy@sdhs.edu', $alias = false);
+ }
+
+ protected function getUser($login)
+ {
+ return Db::fetchRow("SELECT login, password, alias, email, token_auth FROM " . Common::prefixTable('user') . " WHERE login = ?", array($login));
+ }
+
+ protected function assertStarkSynchronized($expectedDomain = 'starkindustries.com')
+ {
+ $user = $this->getUser(self::TEST_LOGIN);
+ $this->assertNotEmpty($user);
+ $passwordHelper = new Password();
+ $this->assertTrue($passwordHelper->verify(md5(self::TEST_PASS_LDAP), $user['password']));
+ unset($user['password']);
+ $this->assertEquals(array(
+ 'login' => self::TEST_LOGIN,
+ 'alias' => 'Tony Stark',
+ 'email' => 'billionairephilanthropistplayboy@' . $expectedDomain,
+ 'token_auth' => UsersManagerAPI::getInstance()->getTokenAuth(self::TEST_LOGIN, md5(self::TEST_PASS_LDAP))
+ ), $user);
+ $userMapper = new UserMapper();
+ $this->assertTrue($userMapper->isUserLdapUser(self::TEST_LOGIN));
+ }
+
+ protected function assertRomanovSynchronized($expectedDomain)
+ {
+ $user = $this->getUser('blackwidow');
+ $this->assertNotEmpty($user);
+ unset($user['password']);
+ unset($user['token_auth']);
+ $this->assertEquals(array(
+ 'login' => 'blackwidow',
+ 'alias' => 'Natalia Romanova',
+ 'email' => 'blackwidow@' . $expectedDomain,
+ ), $user);
+ $userMapper = new UserMapper();
+ $this->assertTrue($userMapper->isUserLdapUser('blackwidow'));
+ }
+
+ private function isLdapServerRunning()
+ {
+ $fp = @fsockopen(self::SERVER_HOST_NAME, self::SERVER_PORT, $errno, $errstr, 5);
+ if (empty($fp)) {
+ return false;
+ } else {
+ fclose($fp);
+ return true;
+ }
+ }
+
+ public function provideContainerConfig()
+ {
+ return array(
+ 'Psr\Log\LoggerInterface' => \DI\get('Monolog\Logger'),
+ );
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/LdapUserSynchronizationTest.php b/tests/Integration/LdapUserSynchronizationTest.php
new file mode 100644
index 0000000..4afb034
--- /dev/null
+++ b/tests/Integration/LdapUserSynchronizationTest.php
@@ -0,0 +1,313 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\Access;
+use Piwik\Auth\Password;
+use Piwik\Config;
+use Piwik\Db;
+use Piwik\Common;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+use Piwik\Plugins\LoginLdap\Auth\LdapAuth;
+use Piwik\SettingsPiwik;
+use Piwik\Tests\Framework\Fixture;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_LdapUserSynchronizationTest
+ */
+class LdapUserSynchronizationTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ // create extra sites that users won't have access to
+ Fixture::createWebsite('2013-01-01 00:00:00');
+ Fixture::createWebsite('2013-01-01 00:00:00');
+ Fixture::createWebsite('2013-01-01 00:00:00');
+
+ Access::getInstance()->setSuperUserAccess(false);
+ }
+
+ public function tearDown()
+ {
+ Access::getInstance()->setSuperUserAccess(true);
+
+ parent::tearDown();
+ }
+
+ public function test_PiwikUserIsCreated_WhenLdapLoginSucceeds_ButPiwikUserDoesntExist()
+ {
+ $this->authenticateViaLdap();
+
+ $this->assertStarkSynchronized();
+
+ $superusers = $this->getSuperUsers();
+ $this->assertEquals(array(), $superusers);
+
+ $access = $this->getAccessFor(self::TEST_LOGIN); // access added due to new_user_default_sites_view_access option
+ $this->assertEquals(array(
+ array('site' => '1', 'access' => 'view'),
+ array('site' => '2', 'access' => 'view')
+ ), $access);
+ }
+
+ public function test_PiwikUserIsCreated_WithEmailSuffixed_ButNotUser_IfShouldAppendEmailIsOff()
+ {
+ Config::getInstance()->LoginLdap['append_user_email_suffix_to_username'] = 0;
+ Config::getInstance()->LoginLdap['user_email_suffix'] = '@matthers.com';
+
+ $this->authenticateViaLdap('blackwidow', 'redledger');
+
+ $this->assertRomanovSynchronized('matthers.com');
+ }
+
+ public function test_PiwikUserIsCreatedWithAccessToAllSites_WhenLdapLoginSucceeds_AndDefaultSitesToAddIsAll()
+ {
+ Config::getInstance()->LoginLdap['new_user_default_sites_view_access'] = 'all';
+
+ $this->authenticateViaLdap();
+
+ $this->assertStarkSynchronized();
+
+ $access = $this->getAccessFor(self::TEST_LOGIN); // access added due to new_user_default_sites_view_access option
+ $this->assertEquals(array(
+ array('site' => '1', 'access' => 'view'),
+ array('site' => '2', 'access' => 'view'),
+ array('site' => '3', 'access' => 'view'),
+ array('site' => '4', 'access' => 'view'),
+ array('site' => '5', 'access' => 'view'),
+ array('site' => '6', 'access' => 'view')
+ ), $access);
+ }
+
+ public function test_PiwikUserIsNotCreated_IfPiwikUserAlreadyExists()
+ {
+ Access::getInstance()->setSuperUserAccess(true);
+ UsersManagerAPI::getInstance()->addUser(self::TEST_LOGIN, self::TEST_PASS, 'billionairephilanthropistplayboy@starkindustries.com', $alias = false);
+ Access::getInstance()->setSuperUserAccess(false);
+
+ $this->authenticateViaLdap();
+
+ $user = Db::fetchRow("SELECT login, password, alias, email, token_auth FROM " . Common::prefixTable('user') . " WHERE login = ?", array(self::TEST_LOGIN));
+ $this->assertNotEmpty($user);
+ $passwordHelper = new Password();
+ $this->assertTrue($passwordHelper->verify(md5(self::TEST_PASS), $user['password']));
+ unset($user['password']);
+ $this->assertEquals(array(
+ 'login' => self::TEST_LOGIN,
+ 'alias' => self::TEST_LOGIN,
+ 'email' => 'billionairephilanthropistplayboy@starkindustries.com',
+ 'token_auth' => UsersManagerAPI::getInstance()->getTokenAuth(self::TEST_LOGIN, md5(self::TEST_PASS))
+ ), $user);
+
+ $this->assertNoAccessInDb();
+ }
+
+ public function test_PiwikUserIsUpdated_IfLdapUserAlreadySynchronized_ButLdapUserInfoIsDifferent()
+ {
+ Access::doAsSuperUser(function () {
+ UsersManagerAPI::getInstance()->addUser(
+ LdapUserSynchronizationTest::TEST_LOGIN, LdapUserSynchronizationTest::TEST_PASS_LDAP, 'something@domain.com', $alias = false, $isPasswordHashed = false);
+ });
+
+ $userMapper = new UserMapper();
+ $userMapper->markUserAsLdapUser(self::TEST_LOGIN);
+
+ $this->authenticateViaLdap();
+
+ $this->assertStarkSynchronized();
+ }
+
+ public function test_LdapSuperUserHasSuperUserAccess_WhenUserIsSynchronized()
+ {
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap(self::TEST_SUPERUSER_LOGIN, self::TEST_SUPERUSER_PASS);
+
+ $superusers = $this->getSuperUsers();
+ $this->assertEquals(array(self::TEST_SUPERUSER_LOGIN), $superusers);
+ }
+
+ public function test_AdminAndViewAccessAddedForLdapUser_WhenLdapUserHasAccess_AndUserIsSynchronizedForFirstTime()
+ {
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap();
+
+ $this->assertStarkAccessSynchronized();
+ }
+
+ public function test_AdminAndViewAccessUpdated_WhenLdapUserALreadySynchronized_ButLdapAccessInfoIsDifferent()
+ {
+ $this->enableAccessSynchronization();
+
+ Access::doAsSuperUser(function () {
+ UsersManagerAPI::getInstance()->addUser(
+ LdapUserSynchronizationTest::TEST_LOGIN, md5('anypass'), 'something@domain.com', $alias = false, $isPasswordHashed = true);
+ UsersManagerAPI::getInstance()->setUserAccess(LdapUserSynchronizationTest::TEST_LOGIN, 'view', array(4,5));
+ UsersManagerAPI::getInstance()->setUserAccess(LdapUserSynchronizationTest::TEST_LOGIN, 'admin', array(6));
+ });
+
+ $this->authenticateViaLdap();
+
+ $this->assertStarkAccessSynchronized();
+ }
+
+ public function test_AdminAndViewAccessSynchronized_WhenLdapAccessInfoPresent_AndInstanceNameUsed()
+ {
+ Config::getInstance()->LoginLdap['instance_name'] = 'myPiwik';
+
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap('blackwidow', 'redledger');
+
+ $access = $this->getAccessFor('blackwidow');
+ $this->assertEquals(array(
+ array('site' => '1', 'access' => 'view'),
+ array('site' => '2', 'access' => 'view'),
+ array('site' => '3', 'access' => 'admin'),
+ array('site' => '4', 'access' => 'admin'),
+ ), $access);
+ }
+
+ public function test_SuperUserAccessSynchronized_WhenLdapAccessInfoPresent_AndInstanceNameUsed_AndUserIsSuperUser()
+ {
+ Config::getInstance()->LoginLdap['instance_name'] = 'myPiwik';
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap('thor', 'bilgesnipe');
+
+ $superusers = $this->getSuperUsers();
+ $this->assertEquals(array('thor'), $superusers);
+ }
+
+ public function test_AdminAndViewAccessSynchronized_WhenLdapAccessInfoPresent_AndInstancePiwikUrlUsed()
+ {
+ $this->setPiwikInstanceUrl('http://localhost/');
+ Config::getInstance()->LoginLdap['ldap_superuser_access_field'] = 'isasuperuser'; // disable superuser check so we can check user's normal access
+
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap('thor', 'bilgesnipe');
+
+ $access = $this->getAccessFor('thor');
+ $this->assertEquals(array(
+ array('site' => '1', 'access' => 'view'),
+ array('site' => '2', 'access' => 'view'),
+ array('site' => '3', 'access' => 'admin'),
+ array('site' => '4', 'access' => 'admin'),
+ ), $access);
+ }
+
+ public function test_SuperUserAccessSynchronized_WhenLdapAccessInfoPresent_AndInstancePiwikUrlUsed_AndUserIsSuperUser()
+ {
+ $this->setPiwikInstanceUrl('http://localhost/');
+ $this->enableAccessSynchronization();
+
+ $this->authenticateViaLdap('thor', 'bilgesnipe');
+
+ $superusers = $this->getSuperUsers();
+ $this->assertEquals(array('thor'), $superusers);
+ }
+
+ public function test_RandomPasswordGenerated()
+ {
+ $passwordManager = new Password();
+
+ $this->authenticateViaLdap();
+
+ $user = $this->getUser(self::TEST_LOGIN);
+
+ $this->assertTrue($passwordManager->verify(md5(self::TEST_PASS_LDAP), $user['password']));
+
+ // test that password doesn't change after re-synchronizing
+ $this->authenticateViaLdap();
+
+ $userAgain = $this->getUser(self::TEST_LOGIN);
+
+ $this->assertTrue($passwordManager->verify(md5(self::TEST_PASS_LDAP), $userAgain['password']));
+ }
+
+ public function test_CorrectExistingUserUpdated_WhenUserEmailSuffixUsed()
+ {
+ Config::getInstance()->LoginLdap['user_email_suffix'] = '@xmansion.org';
+
+ // authenticate via ldap to add the user w/ the email suffix
+ $this->authenticateViaLdap($login = 'rogue', $pass = 'cher');
+
+ $user = $this->getUser('rogue@xmansion.org');
+
+ $this->assertNotEmpty($user);
+
+ // authenticate again to make sure the correct user is updated and we didn't try to add again
+ $this->authenticateViaLdap($login = 'rogue', $pass = 'cher');
+ }
+
+ private function assertNoAccessInDb()
+ {
+ $access = $this->getAccessFor(self::TEST_LOGIN);
+ $this->assertEquals(array(), $access);
+
+ $superusers = $this->getSuperUsers();
+ $this->assertEquals(array(), $superusers);
+ }
+
+ private function getAccessFor($login)
+ {
+ return Access::doAsSuperUser(function () use ($login) {
+ return UsersManagerAPI::getInstance()->getSitesAccessFromUser($login);
+ });
+ }
+
+ private function getSuperUsers()
+ {
+ $result = array();
+ foreach (Db::fetchAll("SELECT login FROM " . Common::prefixTable('user') . " WHERE superuser_access = 1") as $row) {
+ $result[] = $row['login'];
+ }
+ return $result;
+ }
+
+ private function authenticateViaLdap($login = self::TEST_LOGIN, $pass = self::TEST_PASS)
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin($login);
+ $ldapAuth->setPassword($pass);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+
+ return $authResult;
+ }
+
+ private function enableAccessSynchronization()
+ {
+ Config::getInstance()->LoginLdap['enable_synchronize_access_from_ldap'] = 1;
+ }
+
+ private function assertStarkAccessSynchronized()
+ {
+ $access = $this->getAccessFor(self::TEST_LOGIN);
+
+ $this->assertEquals(array(
+ array('site' => '1', 'access' => 'view'),
+ array('site' => '2', 'access' => 'view'),
+ array('site' => '3', 'access' => 'admin')
+ ), $access);
+ }
+
+ private function setPiwikInstanceUrl($url)
+ {
+ SettingsPiwik::overwritePiwikUrl($url);
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/MultipleServersTest.php b/tests/Integration/MultipleServersTest.php
new file mode 100644
index 0000000..cd15800
--- /dev/null
+++ b/tests/Integration/MultipleServersTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\Auth\LdapAuth;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_MultipleServersTest
+ */
+class MultipleServersTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap_dummyserver1 = array(
+ 'hostname' => "notanldaphost.com",
+ 'port' => self::SERVER_PORT,
+ 'base_dn' => self::SERVER_BASE_DN,
+ 'admin_user' => 'cn=fury,' . self::SERVER_BASE_DN,
+ 'admin_pass' => 'secrets'
+ );
+ Config::getInstance()->LoginLdap_dummyserver2 = array(
+ 'hostname' => "localhost",
+ 'port' => 999,
+ 'base_dn' => self::SERVER_BASE_DN,
+ 'admin_user' => 'cn=fury,' . self::SERVER_BASE_DN,
+ 'admin_pass' => 'secrets'
+ );
+ }
+
+ public function testAuthenticateSucceedsWhenFirstServerWorksButOthersFailToConnect()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('testserver', 'dummyserver1', 'dummyserver2');
+
+ $this->doAuthTest();
+ }
+
+ public function testAuthenticateSucceedsWhenOneServerSucceedsButOthersFailToConnect()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('dummyserver1', 'testserver', 'dummyserver2');
+
+ $this->doAuthTest();
+
+ Config::getInstance()->LoginLdap['servers'] = array('dummyserver1', 'dummyserver2', 'testserver');
+
+ $this->doAuthTest();
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException
+ * @expectedExceptionMessageContains Could not connect to any of the
+ */
+ public function testAuthenticateFailsWhenAllServersFailToConnect()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('dummyserver1', 'dummyserver2');
+
+ $this->doAuthTest($expectCode = 0);
+ }
+
+ private function doAuthTest($expectCode = 1)
+ {
+ $ldapAuth = LdapAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals($expectCode, $authResult->getCode());
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/SynchronizedAuthTest.php b/tests/Integration/SynchronizedAuthTest.php
new file mode 100644
index 0000000..39bbe6c
--- /dev/null
+++ b/tests/Integration/SynchronizedAuthTest.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\Auth\Password;
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\Auth\SynchronizedAuth;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_SynchronizedAuthTest
+ */
+class SynchronizedAuthTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap_brokenserver = array(
+ 'hostname' => "localhost",
+ 'port' => 999,
+ 'base_dn' => self::SERVER_BASE_DN,
+ 'admin_user' => 'cn=fury,' . self::SERVER_BASE_DN,
+ 'admin_pass' => 'secrets'
+ );
+ }
+
+ public function test_SynchronizedLdapUsersCanLogin_WithoutConnectingToLdap_WhenUsersExistInPiwikDB()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addPreSynchronizedUser();
+
+ $this->doAuthTest($code = 1);
+ }
+
+ public function test_NormalPiwikUsersCanLogin_WithoutConnectingToLdap()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addNonLdapUsers();
+
+ $this->doAuthTest($code = 42, self::NON_LDAP_USER, self::NON_LDAP_PASS);
+ $this->doAuthTest($code = 1, self::NON_LDAP_NORMAL_USER, self::NON_LDAP_NORMAL_PASS);
+ }
+
+ public function test_SynchronizedLdapUsersCanLogin_WithoutConnectingToLdap_ByTokenAuth()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addPreSynchronizedUser();
+
+ $this->doAuthTestByTokenAuth($code = 1);
+ }
+
+ public function test_NormalPiwikUsersCanLogin_WithoutConnectingToLdap_ByTokenAuth()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addNonLdapUsers();
+
+ $this->doAuthTestByTokenAuth($code = 42, self::NON_LDAP_USER, self::NON_LDAP_PASS);
+ $this->doAuthTestByTokenAuth($code = 1, self::NON_LDAP_NORMAL_USER, self::NON_LDAP_NORMAL_PASS);
+ }
+
+ public function test_SynchronizedLdapUsersCanLogin_WithoutConnectingToLdap_ByPasswordHash()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addPreSynchronizedUser();
+
+ $this->doAuthTestByPasswordHash($code = 1);
+ }
+
+ public function test_NormalPiwikUsersCanLogin_WithoutConnectingToLdap_ByPasswordHash()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->addNonLdapUsers();
+
+ $this->doAuthTestByPasswordHash($code = 42, self::NON_LDAP_USER, self::NON_LDAP_PASS);
+ $this->doAuthTestByPasswordHash($code = 1, self::NON_LDAP_NORMAL_USER, self::NON_LDAP_NORMAL_PASS);
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\LoginLdap\Ldap\Exceptions\ConnectionException
+ */
+ public function test_LdapUsersCannotLogin_IfUnsynchronized_AndLdapServerBroken()
+ {
+ Config::getInstance()->LoginLdap['servers'] = array('brokenserver');
+
+ $this->doAuthTest($code = 0);
+ }
+
+ public function test_LdapUserCannotLogin_IfUserNotInDb_SynchronizingAfterLoginDisabled()
+ {
+ Config::getInstance()->LoginLdap['synchronize_users_after_login'] = 0;
+
+ $this->doAuthTest($code = 0);
+ }
+
+ public function test_LdapUserCannotLogin_IfUserNotInDb_AndAuthenticatingByTokenAuth()
+ {
+ Config::getInstance()->LoginLdap['synchronize_users_after_login'] = 0;
+
+ $this->doAuthTestByTokenAuth($code = 0);
+ }
+
+ public function test_LdapUserCannotLogin_IfUserNotInDb_AndAuthtenticatingByPasswordHash()
+ {
+ Config::getInstance()->LoginLdap['synchronize_users_after_login'] = 0;
+
+ $this->doAuthTestByPasswordHash($code = 0);
+ }
+
+ public function test_AuthenticationFails_WhenUserNotInDb_AndUserNotInLdap()
+ {
+ $this->doAuthTest($code = 0, self::NON_LDAP_USER, self::NON_LDAP_PASS);
+ }
+
+ public function test_AuthenticationFails_WhenUserNotInDb_AndUserInLdap_AndOnlyTokenAuthTested()
+ {
+ $this->doAuthTest($code = 0, self::TEST_LOGIN, null, $this->getLdapUserTokenAuth());
+ }
+
+ public function test_LdapUserPasswordUpdated_AfterSuccessfulLoginViaLdap()
+ {
+ $this->addLdapUserWithWrongPassword();
+
+ $this->doAuthTest($code = 1);
+
+ $user = $this->getUser(self::TEST_LOGIN);
+
+ $passwordHelper = new Password();
+ $this->assertTrue($passwordHelper->verify(md5(self::TEST_PASS), $user['password']));
+
+ $newTokenAuth = $this->getLdapUserTokenAuth();
+ $this->assertEquals($newTokenAuth, $user['token_auth']);
+ }
+
+ private function addPreSynchronizedUser($pass = self::TEST_PASS)
+ {
+ UsersManagerAPI::getInstance()->addUser(
+ self::TEST_LOGIN,
+ $pass,
+ 'billionairephilanthropistplayboy@starkindustries.com',
+ 'Tony Stark'
+ );
+
+ $userMapper = new UserMapper();
+ $userMapper->markUserAsLdapUser(self::TEST_LOGIN);
+ }
+
+ private function addLdapUserWithWrongPassword()
+ {
+ $this->addPreSynchronizedUser('averywrongpassword');
+ }
+
+ private function doAuthTest($expectCode, $login = self::TEST_LOGIN, $pass = self::TEST_PASS, $token_auth = null)
+ {
+ $auth = SynchronizedAuth::makeConfigured();
+ if (!empty($login)) {
+ $auth->setLogin($login);
+ }
+ if (!empty($pass)) {
+ $auth->setPassword($pass);
+ }
+ if (!empty($token_auth)) {
+ $auth->setTokenAuth($token_auth);
+ }
+ $result = $auth->authenticate();
+
+ $this->assertEquals($expectCode, $result->getCode());
+ }
+
+ private function doAuthTestByTokenAuth($expectCode, $login = self::TEST_LOGIN, $pass = self::TEST_PASS)
+ {
+ $tokenAuth = UsersManagerAPI::getInstance()->getTokenAuth($login, md5($pass));
+
+ $auth = SynchronizedAuth::makeConfigured();
+ $auth->setLogin($login);
+ $auth->setTokenAuth($tokenAuth);
+ $result = $auth->authenticate();
+
+ $this->assertEquals($expectCode, $result->getCode());
+ }
+
+ private function doAuthTestByPasswordHash($expectCode, $login = self::TEST_LOGIN, $pass = self::TEST_PASS)
+ {
+ $auth = SynchronizedAuth::makeConfigured();
+ $auth->setLogin($login);
+ $auth->setPasswordHash(md5($pass));
+ $result = $auth->authenticate();
+
+ $this->assertEquals($expectCode, $result->getCode());
+ }
+
+ private function getLdapUserTokenAuth()
+ {
+ return UsersManagerAPI::getInstance()->getTokenAuth(self::TEST_LOGIN, md5(self::TEST_PASS));
+ }
+} \ No newline at end of file
diff --git a/tests/Integration/WebServerAuthTest.php b/tests/Integration/WebServerAuthTest.php
new file mode 100644
index 0000000..33f1442
--- /dev/null
+++ b/tests/Integration/WebServerAuthTest.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Integration;
+
+use Piwik\AuthResult;
+use Piwik\Config;
+use Piwik\Db;
+use Piwik\Plugins\LoginLdap\Auth\WebServerAuth;
+use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Integration
+ * @group LoginLdap_WebServerAuthTest
+ */
+class WebServerAuthTest extends LdapIntegrationTest
+{
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->addPreexistingSuperUser();
+ }
+
+ public function test_WebServerAuth_Works_IfUserExists_RegardlessOfPassword()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+
+ $_SERVER['REMOTE_USER'] = self::TEST_LOGIN;
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $ldapAuth->setPassword('slkdjfdslf');
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+ }
+
+ public function test_WebServerAuth_Fails_IfUserDoesNotExist()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+
+ $_SERVER['REMOTE_USER'] = 'abcdefghijk';
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_WebServerAuth_Fails_IfUserIsNotPartOfRequiredGroup()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+ Config::getInstance()->LoginLdap['memberOf'] = "cn=S.H.I.E.L.D.," . self::SERVER_BASE_DN;
+
+ $_SERVER['REMOTE_USER'] = self::TEST_LOGIN;
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_WebServerAuth_Fails_IfUserIsNotMatchedByCustomFilter()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+ Config::getInstance()->LoginLdap['filter'] = "(mobile=none)";
+
+ $_SERVER['REMOTE_USER'] = self::TEST_LOGIN;
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_WebServerAuth_Fails_IfUserNoRemoteUserExists_AndNoUserSpecifiedThroughAuth()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+
+ unset($_SERVER['REMOTE_USER']);
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(0, $authResult->getCode());
+ }
+
+ public function test_WebServerAuth_UsesCorrectFallbackAuth_IfNoRemoteUserExists_AndAuthDetailsSpecified()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+
+ unset($_SERVER['REMOTE_USER']);
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_LOGIN);
+ $ldapAuth->setPassword(self::TEST_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(1, $authResult->getCode());
+
+ }
+
+ public function test_WebServerAuth_ReturnsCorrectCodeForSuperUsers()
+ {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+
+ $_SERVER['REMOTE_USER'] = self::TEST_SUPERUSER_LOGIN;
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+ }
+
+ public function test_SuperUsersCanLogin_IfWebServerAuthUsed_AndWebServerAuthSetupIncorrectly()
+ {
+ unset($_SERVER['REMOTE_USER']);
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_SUPERUSER_LOGIN);
+ $ldapAuth->setPassword(self::TEST_SUPERUSER_PASS);
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+
+ $ldapAuth = WebServerAuth::makeConfigured();
+ $ldapAuth->setLogin(self::TEST_SUPERUSER_LOGIN);
+ $ldapAuth->setTokenAuth(UsersManagerAPI::getInstance()->getTokenAuth(self::TEST_SUPERUSER_LOGIN, md5(self::TEST_SUPERUSER_PASS)));
+ $authResult = $ldapAuth->authenticate();
+
+ $this->assertEquals(AuthResult::SUCCESS_SUPERUSER_AUTH_CODE, $authResult->getCode());
+ }
+} \ No newline at end of file
diff --git a/tests/Mocks/LdapFunctions.php b/tests/Mocks/LdapFunctions.php
new file mode 100644
index 0000000..2437f2f
--- /dev/null
+++ b/tests/Mocks/LdapFunctions.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\Ldap;
+
+// mocks ldap_* functions for Ldap\Client class
+class LdapFunctions
+{
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ public static $phpUnitMock;
+
+ public static function __callStatic($name, $arguments)
+ {
+ if (isset(self::$phpUnitMock)) {
+ return call_user_func_array(array(self::$phpUnitMock, $name), $arguments);
+ } else {
+ return call_user_func_array("\\" . $name, $arguments);
+ }
+ }
+}
+
+function ldap_connect($hostname = null, $port = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_connect($hostname, $port);
+}
+
+function ldap_set_option($connection = null, $optionName = null, $optionValue = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_set_option($connection, $optionName, $optionValue);
+}
+
+function ldap_error($link_identifier) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_error($link_identifier);
+}
+
+function ldap_bind($connection = null, $resourceDn = null, $password = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_bind($connection, $resourceDn, $password);
+}
+
+function ldap_search($connection = null, $baseDn = null, $filter = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_search($connection, $baseDn, $filter);
+}
+
+function ldap_get_entries($connection = null, $searchResultResource = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_get_entries($connection, $searchResultResource);
+}
+
+function ldap_close($connection = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_close($connection);
+}
+
+function ldap_count_entries($connection = null, $searchResultResource = null) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ return LdapFunctions::ldap_count_entries($connection, $searchResultResource);
+}
+
+if(!defined('LDAP_OPT_PROTOCOL_VERSION')) {
+ define('LDAP_OPT_PROTOCOL_VERSION', 1);
+}
+if(!defined('LDAP_OPT_REFERRALS')) {
+ define('LDAP_OPT_REFERRALS', 1);
+}
+if(!defined('LDAP_OPT_NETWORK_TIMEOUT')) {
+ define('LDAP_OPT_NETWORK_TIMEOUT', 1);
+}
+
diff --git a/tests/System/SystemAuthTest.php b/tests/System/SystemAuthTest.php
new file mode 100644
index 0000000..4daf5bb
--- /dev/null
+++ b/tests/System/SystemAuthTest.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\LoginLdap\tests\System;
+
+use Piwik\AuthResult;
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\Plugins\LoginLdap\LoginLdap;
+use Piwik\Plugins\LoginLdap\Auth\LdapAuth;
+use Piwik\Plugins\LoginLdap\Auth\SynchronizedAuth;
+use Piwik\Plugins\LoginLdap\Auth\WebServerAuth;
+use Piwik\Plugins\LoginLdap\tests\Integration\LdapIntegrationTest;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestingEnvironmentVariables;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_System
+ * @group LoginLdap_SystemAuthTest
+ */
+class SystemAuthTest extends LdapIntegrationTest
+{
+ public function getAuthModesToTest()
+ {
+ return array(
+ array('ldap_only'),
+ array('synchronized'),
+ array('web_server'),
+ );
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+
+ unset($_SERVER['REMOTE_USER']);
+ }
+
+ /**
+ * @dataProvider getAuthModesToTest
+ */
+ public function test_LdapAuthentication_WorksForNormalTrackingRequest($authStrategy)
+ {
+ $this->setUpLdap($authStrategy);
+
+ $superUserTokenAuth = $this->getSuperUserTokenAuth();
+
+ $date = Date::factory('2015-01-01 00:00:00');
+
+ $tracker = Fixture::getTracker($idSite = 1, $date->addHour(1)->getDatetime());
+ $tracker->setTokenAuth($superUserTokenAuth);
+
+ $tracker->setUrl('http://shield.org/protocol/theta');
+ Fixture::checkResponse($tracker->doTrackPageView('I Am A Robot Tourist'));
+
+ // authentication is required to track dates in the past, so to verify we
+ // authenticated, we check the tracked visit times
+ $expectedDateTimes = array('2015-01-01 01:00:00');
+ $actualDateTimes = $this->getVisitDateTimes();
+
+ $this->assertEquals($expectedDateTimes, $actualDateTimes);
+ }
+
+ /**
+ * @dataProvider getAuthModesToTest
+ */
+ public function test_LdapAuthentication_WorksDuringBulkTracking($authStrategy)
+ {
+ $this->setUpLdap($authStrategy);
+
+ $superUserTokenAuth = $this->getSuperUserTokenAuth();
+
+ $date = Date::factory('2015-01-01 00:00:00');
+
+ $tracker = Fixture::getTracker($idSite = 1, $date->getDatetime());
+ $tracker->setTokenAuth($superUserTokenAuth);
+ $tracker->enableBulkTracking();
+
+ $tracker->setForceVisitDateTime($date->getDatetime());
+ $tracker->setUrl('http://shield.org/level/10/dandr/pcoulson');
+ $tracker->doTrackPageView('Death & Recovery');
+
+ $tracker->setForceVisitDateTime($date->addHour(1)->getDatetime());
+ $tracker->setUrl('http://shield.org/logout');
+ $tracker->doTrackPageView('Going dark');
+
+ Fixture::checkBulkTrackingResponse($tracker->doBulkTrack());
+
+ // authentication is required to track dates in the past, so to verify we
+ // authenticated, we check the tracked visit times
+ $expectedDateTimes = array('2015-01-01 00:00:00', '2015-01-01 01:00:00');
+ $actualDateTimes = $this->getVisitDateTimes();
+
+ $this->assertEquals($expectedDateTimes, $actualDateTimes);
+ }
+
+ private function getVisitDateTimes()
+ {
+ $rows = Db::fetchAll("SELECT visit_last_action_time FROM " . Common::prefixTable('log_visit')
+ . " ORDER BY visit_last_action_time ASC");
+
+ $dates = array();
+ foreach ($rows as $row) {
+ $dates[] = $row['visit_last_action_time'];
+ }
+ return $dates;
+ }
+
+ private function setUpLdap($authStrategy)
+ {
+ $testVars = new TestingEnvironmentVariables();
+ $configOverride = $testVars->configOverride;
+
+ if ($authStrategy == 'ldap_only') {
+ Config::getInstance()->LoginLdap['use_ldap_for_authentication'] = 1;
+ $configOverride['LoginLdap']['use_ldap_for_authentication'] = 1;
+ } else if ($authStrategy == 'synchronized') {
+ Config::getInstance()->LoginLdap['use_ldap_for_authentication'] = 0;
+ $configOverride['LoginLdap']['use_ldap_for_authentication'] = 0;
+ } else if ($authStrategy == 'web_server') {
+ Config::getInstance()->LoginLdap['use_webserver_auth'] = 1;
+ $configOverride['LoginLdap']['use_webserver_auth'] = 1;
+ } else {
+ throw new \Exception("Unknown LDAP auth strategy $authStrategy.");
+ }
+
+ $configOverride['Tracker']['debug_on_demand'] = 1;
+
+ $testVars->configOverride = $configOverride;
+ $testVars->save();
+
+ // make sure our superuser is synchronized before hand
+ $this->authenticateUserOnce($authStrategy);
+ }
+
+ private function getSuperUserTokenAuth()
+ {
+ return Db::fetchOne("SELECT token_auth FROM `" . Common::prefixTable('user')
+ . "` WHERE superuser_access = 1 AND login = ?", array(self::TEST_SUPERUSER_LOGIN));
+ }
+
+ private function authenticateUserOnce($authStrategy)
+ {
+ $auth = null;
+ if ($authStrategy == 'ldap_only') {
+ $auth = LdapAuth::makeConfigured();
+ } else if ($authStrategy == 'synchronized') {
+ $auth = SynchronizedAuth::makeConfigured();
+ } else if ($authStrategy == 'web_server') {
+ $auth = WebServerAuth::makeConfigured();
+
+ $_SERVER['REMOTE_USER'] = self::TEST_SUPERUSER_LOGIN;
+ } else {
+ throw new \Exception("Unknown LDAP auth strategy $authStrategy.");
+ }
+
+ $auth->setLogin(self::TEST_SUPERUSER_LOGIN);
+ $auth->setPassword(self::TEST_SUPERUSER_PASS);
+ $result = $auth->authenticate();
+
+ $this->assertNotEquals(AuthResult::FAILURE, $result->getCode());
+
+ \Piwik\Plugins\UsersManager\API::getInstance()->setSuperUserAccess(self::TEST_SUPERUSER_LOGIN, true);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage LoginLdap_LdapUserCantChangePassword
+ */
+ public function testLdapUserPassChange()
+ {
+ $auth = LdapAuth::makeConfigured();
+ $auth->setLogin(self::TEST_LOGIN);
+ $auth->setPassword(self::TEST_PASS);
+ $result = $auth->authenticate();
+
+ $this->assertNotEquals(AuthResult::FAILURE, $result->getCode());
+
+ $loginLdap = new LoginLdap();
+ $loginLdap->disablePasswordChangeForLdapUsers($auth);
+ }
+}
diff --git a/tests/UI/Admin_spec.js b/tests/UI/Admin_spec.js
new file mode 100644
index 0000000..0ed6d9d
--- /dev/null
+++ b/tests/UI/Admin_spec.js
@@ -0,0 +1,53 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * LoginLdap admin page screenshot tests.
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("LoginLdap_Admin", function () {
+ this.timeout(0);
+
+ this.fixture = "Piwik\\Tests\\Fixtures\\OneVisitorTwoVisits";
+
+ before(function () {
+ testEnvironment.pluginsToLoad = ['LoginLdap'];
+ testEnvironment.configOverride = {
+ LoginLdap: {
+ servers: ['testserver'],
+ new_user_default_sites_view_access: '1,2',
+ enable_synchronize_access_from_ldap: 1
+ },
+ LoginLdap_testserver: {
+ hostname: 'localhost',
+ port: 389,
+ base_dn: 'dc=avengers,dc=shield,dc=org',
+ admin_user: 'cn=fury,dc=avengers,dc=shield,dc=org',
+ admin_pass: 'secrets'
+ }
+ };
+ testEnvironment.save();
+ });
+
+ var ldapAdminUrl = "?module=LoginLdap&action=admin&idSite=1&period=day&date=yesterday";
+
+ it("should load correctly and allow testing the filter and group fields", function (done) {
+ expect.screenshot('admin_page').to.be.captureSelector('#content', function (page) {
+ page.load(ldapAdminUrl, 2000);
+
+ page.sendKeys('input#required_member_of', 'a');
+ page.sendKeys('input#ldap_user_filter', 'a');
+
+ page.evaluate(function () {
+ $('input#required_member_of').val('cn=avengers,dc=avengers,dc=shield,dc=org').trigger('input');
+ $('input#ldap_user_filter').val('(objectClass=person)').trigger('input');
+ });
+
+ page.evaluate(function () {
+ $('[piwik-login-ldap-testable-field] [piwik-save-button] input').click();
+ }, 1000);
+ }, done);
+ });
+});
diff --git a/tests/UI/expected-ui-screenshots/LoginLdap_Admin_admin_page.png b/tests/UI/expected-ui-screenshots/LoginLdap_Admin_admin_page.png
new file mode 100644
index 0000000..f896243
--- /dev/null
+++ b/tests/UI/expected-ui-screenshots/LoginLdap_Admin_admin_page.png
Binary files differ
diff --git a/tests/Unit/LdapClientTest.php b/tests/Unit/LdapClientTest.php
new file mode 100644
index 0000000..0ced099
--- /dev/null
+++ b/tests/Unit/LdapClientTest.php
@@ -0,0 +1,337 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use Piwik\ErrorHandler;
+use Piwik\Plugins\LoginLdap\Ldap\Client as LdapClient;
+use Piwik\Plugins\LoginLdap\Ldap\LdapFunctions;
+use PHPUnit_Framework_TestCase;
+
+require_once PIWIK_INCLUDE_PATH . '/plugins/LoginLdap/tests/Mocks/LdapFunctions.php';
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_LdapClientTest
+ */
+class LdapClientTest extends PHPUnit_Framework_TestCase
+{
+ const ERROR_MESSAGE = "triggered error";
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ LdapFunctions::$phpUnitMock = $this->getMockBuilder('stdClass')
+ ->setMethods( array('ldap_connect', 'ldap_close',
+ 'ldap_bind', 'ldap_search', 'ldap_set_option',
+ 'ldap_get_entries', 'ldap_count_entries'))
+ ->getMock();
+ }
+
+ public function tearDown()
+ {
+ restore_error_handler();
+
+ LdapFunctions::$phpUnitMock = null;
+
+ parent::tearDown();
+ }
+
+ public function test_construction_WithNoArguments_DoesNotConnect()
+ {
+ $ldapClient = new LdapClient();
+
+ $this->assertFalse($ldapClient->isOpen());
+ }
+
+ public function test_construction_WithHostnameAndPort_AttemptsToConnect()
+ {
+ $this->addLdapConnectMethodMock("hostname", 1234);
+
+ $ldapClient = new LdapClient("hostname", 1234);
+
+ $this->assertTrue($ldapClient->isOpen());
+ }
+
+ public function test_connect_Closes_IfConnectionCurrentlyOpen()
+ {
+ $this->addLdapConnectMethodMock();
+
+ LdapFunctions::$phpUnitMock->expects($this->exactly(1))->method('ldap_close')
+ ->withConsecutive(
+ array($this->equalTo("connection_resource_hostname_1234")),
+ array($this->equalTo("connection_resource_hostname2_4567"))
+ );
+
+ $ldapClient = new LdapClient("hostname", 1234);
+ $this->assertTrue($ldapClient->isOpen());
+
+ $ldapClient->connect("hostname2", 4567);
+ $this->assertTrue($ldapClient->isOpen());
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\ErrorException
+ * @expectedExceptionMessage triggered error
+ */
+ public function test_connect_ThrowsPhpErrors()
+ {
+ $this->setPiwikErrorHandling();
+
+ $this->addLdapMethodThatTriggersPhpError('ldap_connect');
+
+ $ldapClient = new LdapClient();
+ $ldapClient->connect("hostname", 1234);
+ }
+
+ public function test_close_Succeeds_IfConnectionAlreadyClosed()
+ {
+ $ldapClient = new LdapClient();
+ $ldapClient->close();
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\ErrorException
+ * @expectedExceptionMessage triggered error
+ */
+ public function test_close_ThrowsPhpErrors()
+ {
+ $this->setPiwikErrorHandling();
+
+ $this->addLdapConnectMethodMock();
+ $this->addLdapMethodThatTriggersPhpError('ldap_close');
+
+ $ldapClient = new LdapClient("hostname", 1234);
+ $ldapClient->close();
+ }
+
+ public function test_bind_ForwardsLdapBindResult()
+ {
+ LdapFunctions::$phpUnitMock->expects($this->once())->method('ldap_bind')->will($this->returnValue("ldap_bind result"));
+
+ $ldapClient = new LdapClient();
+ $result = $ldapClient->bind("resource", "password");
+ $this->assertEquals("ldap_bind result", $result);
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\ErrorException
+ * @expectedExceptionMessage triggered error
+ */
+ public function test_bind_ThrowsPhpErrors()
+ {
+ $this->setPiwikErrorHandling();
+
+ $this->addLdapMethodThatTriggersPhpError('ldap_bind');
+
+ $ldapClient = new LdapClient();
+ $ldapClient->bind("resource", "password");
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\ErrorException
+ * @expectedExceptionMessage triggered error
+ */
+ public function test_fetchAll_ThrowsPhpErrors()
+ {
+ $this->setPiwikErrorHandling();
+
+ $this->addLdapMethodThatTriggersPhpError('ldap_search');
+ $this->addLdapMethodThatTriggersPhpError('ldap_get_entries');
+
+ $ldapClient = new LdapClient();
+ $ldapClient->fetchAll("base dn", "filter");
+ }
+
+ public function test_fetchAll_ReturnsNull_IfLdapSearchFailsSilently()
+ {
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnValue(null));
+
+ $ldapClient = new LdapClient();
+ $result = $ldapClient->fetchAll("base dn", "filter");
+
+ $this->assertNull($result);
+ }
+
+ public function test_fetchAll_CorrectlyEscapesFilterParameters()
+ {
+ $escapedFilter = null;
+
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnCallback(
+ function ($conn, $dn, $filter) use (&$escapedFilter) {
+ $escapedFilter = $filter;
+ })
+ );
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_get_entries')->will($this->returnValue("result"));
+
+ $ldapClient = new LdapClient();
+
+ $ldapClient->fetchAll("base dn", "(uid=name?)");
+ $this->assertEquals("(uid=name?)", $escapedFilter);
+
+ $ldapClient->fetchAll("base dn", "(uid=?)", array("na(m)e?'!"));
+ $this->assertEquals('(uid=na\\28m\\29e?\'!)', $escapedFilter);
+
+ $ldapClient->fetchAll("base dn", "(&(uid=?,?)(whatev=?))", array("on()e", "tw?", "(thre"));
+ $this->assertEquals("(&(uid=on\\28\\29e,tw?)(whatev=\\28thre))", $escapedFilter);
+
+ $ldapClient->fetchAll("base dn", "(&(uid=?,?)(whatev=?))", array("t*w()o", "t\\hr??e"));
+ $this->assertEquals("(&(uid=t\\2aw\\28\\29o,t\\5chr??e)(whatev=?))", $escapedFilter);
+ }
+
+ public function getLdapTransformTestData()
+ {
+ return array(
+ array(
+ array('count' => 0),
+ array()
+ ),
+ // test pair
+ array(
+ array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'cn' => array('count' => 1, '0' => 'value'),
+ 'dn' => 'the dn',
+ 0 => 'cn',
+ 1 => 'dn'
+ )
+ ),
+ array(
+ array('cn' => 'value', 'dn' => 'the dn')
+ )
+ ),
+ // test pair
+ array(
+ array(
+ 'count' => 2,
+ 0 => array(
+ 'count' => 2,
+ 0 => 'cn',
+ 1 => 'objectClass',
+ 'cn' => array('count' => 1, '0' => 'value2'),
+ 'objectClass' => array('count' => '1', '0' => 'top'),
+ 'dn' => 'the dn'
+ ),
+ 1 => array(
+ 'count' => 2,
+ 'cn' => array('count' => 2, '0' => 'value3'),
+ 0 => 'objectclass',
+ 'objectclass' => array('count' => '2', '0' => 'top', '1' => 'inetOrgPersion'),
+ 1 => 'cn'
+ )
+ ),
+ array(
+ array('cn' => 'value2', 'objectclass' => 'top', 'dn' => 'the dn'),
+ array('objectclass' => array('top', 'inetOrgPersion'), 'cn' => 'value3'),
+ )
+ ),
+ // test pair
+ array(
+ array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 1,
+ 'superuser' => array(
+ 'count' => 2,
+ 0 => '1',
+ 1 => 'anotherpiwik'
+ ),
+ 0 => 'superuser'
+ )
+ ),
+ array(
+ array('superuser' => array('1', 'anotherpiwik'))
+ )
+ )
+ );
+ }
+
+ /**
+ * @dataProvider getLdapTransformTestData
+ */
+ public function test_fetchAll_CorrectlyProcessesLdapSearchResults($ldapData, $expectedData)
+ {
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnValue("resource"));
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_get_entries')->will($this->returnValue($ldapData));
+
+ $ldapClient = new LdapClient();
+ $result = $ldapClient->fetchAll("base dn", "filter");
+ $this->assertEquals($expectedData, $result);
+ }
+
+ /**
+ * @expectedException \Piwik\Exception\ErrorException
+ * @expectedExceptionMessage triggered error
+ */
+ public function test_count_ThrowsPhpErrors()
+ {
+ $this->setPiwikErrorHandling();
+
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnValue("resource"));
+ $this->addLdapMethodThatTriggersPhpError('ldap_count_entries');
+
+ $ldapClient = new LdapClient();
+ $ldapClient->count("base dn", "filter");
+ }
+
+ /**
+ * @expectedException \Exception
+ */
+ public function test_count_Throws_IfLdapSearchReturnsNull()
+ {
+ $this->setPiwikErrorHandling();
+
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnValue(null));
+
+ $ldapClient = new LdapClient();
+ $ldapClient->count("base dn", "filter");
+ }
+
+ public function test_count_ReturnsCorrectValue()
+ {
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_search')->will($this->returnValue("resource"));
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_count_entries')->will($this->returnValue(8));
+
+ $ldapClient = new LdapClient();
+ $result = $ldapClient->count("base dn", "filter");
+ $this->assertEquals(8, $result);
+ }
+
+ private function addLdapConnectMethodMock($hostname = null, $port = null)
+ {
+ $getConnectionResource = function ($hostname, $port) {
+ return "connection_resource_$hostname" . '_' . $port;
+ };
+
+ $method = LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_connect');
+ if (!empty($hostname) || !empty($port)) {
+ $method = $method->with($this->equalTo($hostname), $this->equalTo($port));
+ }
+ $method->will($this->returnCallback($getConnectionResource));
+
+ LdapFunctions::$phpUnitMock->expects($this->any())->method('ldap_set_option')->will($this->returnValue(null));
+ }
+
+ private function addLdapMethodThatTriggersPhpError($methodName, $returnValue = null)
+ {
+ LdapFunctions::$phpUnitMock->expects($this->any())->method($methodName)->will($this->returnCallback(function () use ($returnValue) {
+ trigger_error(LdapClientTest::ERROR_MESSAGE, E_USER_ERROR);
+ return $returnValue;
+ }));
+ }
+
+ private function setPiwikErrorHandling()
+ {
+ ErrorHandler::registerErrorHandler();
+ error_reporting(E_ALL);
+ }
+}
diff --git a/tests/Unit/LdapUsersTest.php b/tests/Unit/LdapUsersTest.php
new file mode 100644
index 0000000..e2ddca1
--- /dev/null
+++ b/tests/Unit/LdapUsersTest.php
@@ -0,0 +1,522 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use Exception;
+use InvalidArgumentException;
+use Piwik\Plugins\LoginLdap\Ldap\ServerInfo;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+use Piwik\Plugins\LoginLdap\Model\LdapUsers;
+use PHPUnit_Framework_TestCase;
+
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_LdapUsersTest
+ */
+class LdapUsersTest extends PHPUnit_Framework_TestCase
+{
+ const TEST_USER = "rose";
+ const PASSWORD = "bw";
+ const TEST_ADMIN_USER = "who?";
+ const TEST_BASE_DN = 'testbasedn';
+ const TEST_EXTRA_FILTER = '(testfilter)';
+ const TEST_MEMBER_OF = "member";
+ const TEST_MEMBER_OF_Field = "memberOf";
+
+ /**
+ * @var LdapUsers
+ */
+ private $ldapUsers = null;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->ldapUsers = new LdapUsers();
+ $this->ldapUsers->setLdapServers(array(new ServerInfo("localhost", "basedn")));
+ $this->ldapUsers->setLdapUserMapper(new UserMapper());
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function test_authenticate_ThrowsException_IfUsernameEmpty()
+ {
+ $this->ldapUsers->authenticate(null, self::PASSWORD);
+ $this->ldapUsers->authenticate("", self::PASSWORD);
+ }
+
+ public function test_authenticate_ReturnsNull_WhenPasswordIsEmpty()
+ {
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, null);
+ $this->assertNull($result);
+
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, "");
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_CreatesOneClient_WhenNoExistingClientSupplied()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+ $mockLdapClient->expects($this->once())->method('connect');
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD);
+ }
+
+ public function test_authenticate_Fails_WhenUserDoesNotExist()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array()));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD);
+
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_Fails_WhenUserDoesNotExist_AndWebServerAuthUsed()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array()));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = true);
+
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_SucceedsWithoutLdapBind_WhenWebServerAuthUsed_AndUserExists()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function () {
+ static $i = 0;
+
+ ++$i;
+
+ if ($i == 1) {
+ return true;
+ } else {
+ return false; // fail binding after first calls
+ }
+ }));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array(array('uid' => self::TEST_USER))));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = true);
+
+ $this->assertNotNull($result);
+ }
+
+ public function test_authenticate_Succeeds_WhenLdapBindSucceedsAndUserExists()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = false);
+
+ $this->assertNotNull($result);
+ }
+
+ public function test_authenticate_Fails_WhenLdapBindFails()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function () {
+ static $i = 0;
+
+ ++$i;
+
+ if ($i == 1) {
+ return true;
+ } else {
+ return false; // fail binding after first calls
+ }
+ }));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array(array('uid' => self::TEST_USER, 'dn' => 'thedn'))));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = false);
+
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_Fails_WhenLdapUserInfoDoesNotHaveDn()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array(array('uid' => self::TEST_USER))));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = false);
+
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_DoesNotPropagateErrors_WhenErrorsThrownByLdapClient()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function () {
+ throw new \Exception("dummy error");
+ }));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD, $alreadyAuthenticated = false);
+
+ $this->assertNull($result);
+ }
+
+ public function test_authenticate_AddsUsernameSuffix_IfOneIsConfigured()
+ {
+ $adminUserName = null;
+ $userName = null;
+ $filterUsed = null;
+ $filterBind = null;
+
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function ($bindResource) use (&$adminUserName, &$userName) {
+ static $i = 0;
+
+ if ($i == 0) {
+ $adminUserName = $bindResource;
+ } else {
+ $userName = $bindResource;
+ }
+
+ ++$i;
+
+ return true;
+ }));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnCallback(function ($baseDn, $filter, $bind) use (&$filterUsed, &$filterBind) {
+ $filterUsed = $filter;
+ $filterBind = $bind;
+
+ return array(array('uid' => LdapUsersTest::TEST_USER, 'dn' => 'thedn'));
+ }));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->setAuthenticationUsernameSuffix('whoa');
+ $this->ldapUsers->authenticate(self::TEST_USER, self::PASSWORD);
+
+ $this->assertEquals(self::TEST_ADMIN_USER, $adminUserName);
+ $this->assertEquals('thedn', $userName);
+ $this->assertContains("uid=?", $filterUsed);
+ $this->assertEquals(array(self::TEST_USER . 'whoa'), $filterBind);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage Could not bind as LDAP admin.
+ */
+ public function test_getUser_Throws_IfAdminBindFails()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientFailOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->getUser(self::TEST_USER);
+ }
+
+ public function test_getUser_ReturnsNull_WhenThereIsNoUser()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnValue(array()));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->getUser(self::TEST_USER);
+
+ $this->assertNull($result);
+ }
+
+ public function test_getUser_ReturnsTheUser_WhenAUserIsFound()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $result = $this->ldapUsers->getUser(self::TEST_USER);
+
+ $this->assertEquals(array('uid' => self::TEST_USER, 'otherval' => 34, 'dn' => 'thedn'), $result);
+ }
+
+ public function test_getUser_UsesCorrectLDAPFilterAndBaseDn()
+ {
+ $usedBaseDn = null;
+ $usedFilter = null;
+ $filterBind = null;
+
+ $mockLdapClient = $this->makeMockLdapClient();
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnCallback(function ($baseDn, $filter, $bind) use (&$usedBaseDn, &$usedFilter, &$filterBind) {
+ $usedBaseDn = $baseDn;
+ $usedFilter = $filter;
+ $filterBind = $bind;
+
+ return array(array('uid' => LdapUsersTest::TEST_USER));
+ }));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->setAuthenticationLdapFilter(self::TEST_EXTRA_FILTER);
+ $this->ldapUsers->setAuthenticationRequiredMemberOf(self::TEST_MEMBER_OF);
+ $this->ldapUsers->setAuthenticationMemberOfField(self::TEST_MEMBER_OF_Field);
+ $this->ldapUsers->getUser(self::TEST_USER);
+
+ $this->assertEquals(self::TEST_BASE_DN, $usedBaseDn);
+ $this->assertContains(self::TEST_EXTRA_FILTER, $usedFilter);
+ $this->assertContains("(".self::TEST_MEMBER_OF_Field."=?)", $usedFilter);
+ $this->assertContains(self::TEST_MEMBER_OF, $filterBind);
+ }
+
+ public function test_getUser_CreatesOneClient_WhenNoExistingClientSupplied()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+ $mockLdapClient->expects($this->once())->method('connect');
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->ldapUsers->getUser(self::TEST_USER);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage dummy error
+ */
+ public function test_getUser_ThrowsException_WhenLdapClientThrows()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientThrowOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->ldapUsers->getUser(self::TEST_USER);
+ }
+
+ public function test_doWithCllient_CallsCallbackCorrectly_WhenFirstServerConnects()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+ $mockLdapClient->expects($this->once())->method('connect');
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+
+ $serverInfo = new ServerInfo("localhost", 389);
+ $this->ldapUsers->setLdapServers(array($serverInfo));
+
+ $passedLdapUsers = null;
+ $passedClient = null;
+ $passedServerInfo = null;
+ $result = $this->ldapUsers->doWithClient(function ($ldapUsers, $client, $serverInfo)
+ use (&$passedLdapUsers, &$passedClient, &$passedServerInfo) {
+
+ $passedLdapUsers = $ldapUsers;
+ $passedClient = $client;
+ $passedServerInfo = $serverInfo;
+
+ return "test result";
+ });
+
+ $this->assertEquals("test result", $result);
+ $this->assertSame($mockLdapClient, $passedClient);
+ $this->assertSame($this->ldapUsers, $passedLdapUsers);
+ $this->assertSame($serverInfo, $passedServerInfo);
+ }
+
+ public function test_doWithClient_CreatesAClientUsingFirstSuccessfulConnection()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+ $mockLdapClient->expects($this->any())->method('connect')->will($this->returnCallback(function () {
+ static $i = 0;
+
+ ++$i;
+
+ if ($i != 3) {
+ throw new \Exception("fail connection");
+ } else {
+ return;
+ }
+ }));
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+
+ $serverInfos = array(
+ new ServerInfo("localhost1", 1),
+ new ServerInfo("localhost2", 2),
+ new ServerInfo("localhost3", 3)
+ );
+ $this->ldapUsers->setLdapServers($serverInfos);
+
+ $passedLdapUsers = null;
+ $passedClient = null;
+ $passedServerInfo = null;
+ $this->ldapUsers->doWithClient(function ($ldapUsers, $client, $serverInfo)
+ use (&$passedLdapUsers, &$passedClient, &$passedServerInfo) {
+
+ $passedLdapUsers = $ldapUsers;
+ $passedClient = $client;
+ $passedServerInfo = $serverInfo;
+ });
+
+ $this->assertSame($mockLdapClient, $passedClient);
+ $this->assertSame($this->ldapUsers, $passedLdapUsers);
+ $this->assertSame($serverInfos[2], $passedServerInfo);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage test
+ */
+ public function test_doWithClient_PropagatesCallbackExceptions()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+
+ $serverInfo = new ServerInfo("localhost", 389);
+ $this->ldapUsers->setLdapServers(array($serverInfo));
+
+ $this->ldapUsers->doWithClient(function () {
+ throw new Exception("test");
+ });
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage Could not bind as LDAP admin.
+ */
+ public function test_getCountOfUsersMatchingFilter_Throws_IfAdminBindFails()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientFailOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->getCountOfUsersMatchingFilter("dummy filter");
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage dummy error
+ */
+ public function test_getCountOfUsersMatchingFilter_Throws_IfLdapClientConnectThrows()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientThrowOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->getCountOfUsersMatchingFilter("dummy filter");
+ }
+
+ public function test_getCountOfUsersMatchingFilter_ReturnsLdapEntityCount()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = true);
+
+ $passedFilter = null;
+ $mockLdapClient->expects($this->any())->method('count')->will($this->returnCallback(function ($baseDn, $filter) use (&$passedFilter) {
+ $passedFilter = $filter;
+ return 10;
+ }));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $count = $this->ldapUsers->getCountOfUsersMatchingFilter("dummy filter");
+
+ $this->assertEquals("dummy filter", $passedFilter);
+ $this->assertEquals(10, $count);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage Could not bind as LDAP admin.
+ */
+ public function test_getAllUserLogins_Throws_IfAdminBindFails()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientFailOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->getAllUserLogins();
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage dummy error
+ */
+ public function test_getAllUserLogins_Throws_IfLdapClientConnectThrows()
+ {
+ $mockLdapClient = $this->makeMockLdapClient();
+ $this->makeMockLdapClientThrowOnBind($mockLdapClient);
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+ $this->ldapUsers->getAllUserLogins();
+ }
+
+ public function test_getAllUserLogins_ReturnsLdapEntities()
+ {
+ $mockLdapClient = $this->makeMockLdapClient($forSuccess = false);
+
+ $usedFilter = null;
+
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mockLdapClient->expects($this->any())->method('fetchAll')->will($this->returnCallback(function ($baseDn, $filter, $bind) use (&$usedFilter) {
+ $usedFilter = $filter;
+
+ return array(array('uid' => LdapUsersTest::TEST_USER), array('uid' => LdapUsersTest::TEST_ADMIN_USER));
+ }));
+
+ $this->ldapUsers->setLdapClientClass($mockLdapClient);
+ $this->setSingleLdapServer();
+
+ $logins = $this->ldapUsers->getAllUserLogins();
+ $this->assertEquals(array(self::TEST_USER, self::TEST_ADMIN_USER), $logins);
+ }
+
+ private function makeMockLdapClient($forSuccess = false)
+ {
+ $methods = array('__construct', 'connect', 'close', 'bind', 'fetchAll', 'isOpen', 'count');
+
+ $mock = $this->getMockBuilder('Piwik\Plugins\LoginLdap\Ldap\Client')
+ ->disableOriginalConstructor()
+ ->setMethods($methods)
+ ->getMock();
+
+ if ($forSuccess) {
+ $mock->expects($this->any())->method('bind')->will($this->returnValue(true));
+ $mock->expects($this->any())->method('fetchAll')->will($this->returnValue(array(array('uid' => self::TEST_USER, 'otherval' => 34, 'dn' => 'thedn'))));
+ }
+
+ return $mock;
+ }
+
+ private function makeMockLdapClientFailOnBind(\PHPUnit_Framework_MockObject_MockObject $mockLdapClient)
+ {
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function ($bindResource) {
+ if ($bindResource == LdapUsersTest::TEST_ADMIN_USER) {
+ return false;
+ } else {
+ return true;
+ }
+ }));
+ }
+
+ private function makeMockLdapClientThrowOnBind(\PHPUnit_Framework_MockObject_MockObject $mockLdapClient)
+ {
+ $mockLdapClient->expects($this->any())->method('bind')->will($this->returnCallback(function () {
+ throw new Exception("dummy error");
+ }));
+ }
+
+ private function setSingleLdapServer()
+ {
+ $this->ldapUsers->setLdapServers(array(new ServerInfo("localhost", self::TEST_BASE_DN, 389, self::TEST_ADMIN_USER)));
+ }
+} \ No newline at end of file
diff --git a/tests/Unit/UserAccessAttributeParserTest.php b/tests/Unit/UserAccessAttributeParserTest.php
new file mode 100644
index 0000000..c544692
--- /dev/null
+++ b/tests/Unit/UserAccessAttributeParserTest.php
@@ -0,0 +1,308 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use PHPUnit_Framework_TestCase;
+use Piwik\Config;
+use Piwik\Option;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserAccessAttributeParser;
+use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
+use Piwik\SettingsPiwik;
+
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_UserAccessAttributeParserTest
+ */
+class UserAccessAttributeParserTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @var UserAccessAttributeParser
+ */
+ private $userAccessAttributeParser;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap = array();
+
+ $this->setSitesManagerApiMock();
+
+ $this->userAccessAttributeParser = new UserAccessAttributeParser();
+ }
+
+ public function tearDown()
+ {
+ Option::setSingletonInstance(null);
+ }
+
+ public function test_makeConfigured_CreatesCorrectInstance_WhenAllConfigOptionsSpecified()
+ {
+ Config::getInstance()->LoginLdap = array(
+ 'user_access_attribute_server_specification_delimiter' => '#',
+ 'user_access_attribute_server_separator' => '|',
+ 'instance_name' => 'myPiwik'
+ );
+
+ $parser = UserAccessAttributeParser::makeConfigured();
+
+ $this->assertEquals('#', $parser->getServerSpecificationDelimiter());
+ $this->assertEquals('|', $parser->getServerIdsSeparator());
+ $this->assertEquals('myPiwik', $parser->getThisPiwikInstanceName());
+ }
+
+ public function test_makeConfigured_CreatesCorrectInstance_WhenNoConfigOptionsSpecified()
+ {
+ $parser = UserAccessAttributeParser::makeConfigured();
+
+ $this->assertEquals(';', $parser->getServerSpecificationDelimiter());
+ $this->assertEquals(':', $parser->getServerIdsSeparator());
+ $this->assertNull($parser->getThisPiwikInstanceName());
+ }
+
+ public function getInstanceNamesToTest()
+ {
+ return array(
+ array('myPiwik'),
+ array(null)
+ );
+ }
+
+ public function getInstanceUrlVariationsToTest()
+ {
+ return array(
+ array("https://whatever.com", "whatever.com"),
+ array("https://whatever.com", "http://whatever.com"),
+ array("https://whatever.com", "https://whatever.com"),
+ array("https://whatever.com", "https://whatever.com:80"),
+ array("http://whatever.com/what/ever?abc", "whatever.com/what/ever"),
+ array("http://whatever.com/what/ever?abc", "https://whatever.com/what/ever"),
+ array("http://whatever.com/what/ever?abc", "http://whatever.com/what/ever"),
+ array("http://whatever.com/what/ever?abc", "whatever.com/what/ever?def"),
+ array("www.whatever.com", "www.whatever.com"),
+ array("www.whatever.com", "www.whatever.com?abc#def"),
+ array("http://whatever.com:80", "whatever.com"),
+ array("http://whatever.com/", "whatever.com"),
+ array("http://whatever.com/index.php", "whatever.com/index.php"),
+ array("http://whatever.com/index.php", "whatever.com/index.php/"),
+ );
+ }
+
+ /**
+ * @dataProvider getInstanceNamesToTest
+ */
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenNoInstanceUsed($instanceName)
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName($instanceName);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("1,2,3");
+ $this->assertEquals(array(1,2,3), $ids);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("2");
+ $this->assertEquals(array(2), $ids);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute('all');
+ $this->assertEquals(array(1,2,3,4,5,6), $ids);
+ }
+
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenDifferentInstanceNamesUsed()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("myPiwik:1,2;otherPiwik:4;myPiwik:3");
+ $this->assertEquals(array(1,2,3), $ids);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute(" myPiwik : 1,2 ; other:3 ");
+ $this->assertEquals(array(1,2), $ids);
+ }
+
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenAllStringUsed()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("otherPiwik:1,2;myPiwik:all;another:3");
+ $this->assertEquals(array(1,2,3,4,5,6), $ids);
+ }
+
+ /**
+ * @dataProvider getInstanceUrlVariationsToTest
+ */
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenDifferentInstanceUrlUsed($thisUrl, $instanceId)
+ {
+ $this->setThisPiwikUrl($thisUrl);
+ $this->userAccessAttributeParser->setServerIdsSeparator('|');
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute($instanceId . "|1,2,3");
+ $this->assertEquals(array(1,2,3), $ids);
+ }
+
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenAttributeValueIsMalformed_AndMatchingInstanceByUrl()
+ {
+ $this->setThisPiwikUrl("http://whatever.com/a?b=c");
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute(";;whatever.com/a:1,2,3;*{}@@.co?m:1,2;;");
+ $this->assertEquals(array(1,2,3), $ids);
+
+ $this->setThisPiwikUrl("ht??p://@@.com");
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("whatever.com:1,2,3");
+ $this->assertEquals(array(), $ids);
+
+ $this->userAccessAttributeParser->setThisPiwikInstanceName("myPi|wik");
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute(" ; ; myPi|wik : 1,2 ; myPi|wik = view:3,def; myPi | wik:4,5 ; ; ");
+ $this->assertEquals(array(1,2,3), $ids);
+ }
+
+ public function test_getSiteIdsFromAccessAttribute_ReturnsCorrectSiteIdList_WhenCustomDelimitersAreUsed()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+ $this->userAccessAttributeParser->setServerSpecificationDelimiter('#');
+ $this->userAccessAttributeParser->setServerIdsSeparator('|');
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("1,2");
+ $this->assertEquals(array(1,2), $ids);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("wrongPiwik|all#myPiwik|1,2#anotherPiwik|3,4");
+ $this->assertEquals(array(1,2), $ids);
+
+ $this->userAccessAttributeParser->setThisPiwikInstanceName(null);
+
+ $this->setThisPiwikUrl("http://whatever.com:80");
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("whatever.com:80|1,2,3#whatever.com:8080|3,4");
+ $this->assertEquals(array(1,2,3), $ids);
+
+ $ids = $this->userAccessAttributeParser->getSiteIdsFromAccessAttribute("http://whatever.com:801|1,2#whatever.com:80|3,4#http://whatever.com:80|5,6");
+ $this->assertEquals(array(3,4,5,6), $ids);
+ }
+
+ public function test_getSuperUserAccessFromSuperUserAttribute_ReturnsTrue_IfNoInstanceSpecified()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $this->assertTrue($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute(""));
+ $this->assertTrue($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("1"));
+ $this->assertTrue($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("true"));
+ $this->assertTrue($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("tRuE"));
+
+ $this->assertFalse($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("false"));
+ $this->assertFalse($this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("server"));
+ }
+
+ public function test_getSuperUserAccessFromSuperUserAttribute_ReturnsTrue_IfInstanceInSpecifiedList()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("myPiwik");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("somePiwik;myPiwik;anotherPiwik");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("myPiwik : somePiwik : anotherPiwik");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $this->userAccessAttributeParser->setThisPiwikInstanceName(null);
+ $this->userAccessAttributeParser->setServerIdsSeparator('|');
+ $this->setThisPiwikUrl("https://whatever.com/piwik");
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("whatever.com/piwik");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("http://whatever.com/piwik|anotherpiwik.com");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute(" anotherpiwik.com | https://whatever.com/piwik ");
+ $this->assertTrue($hasSuperUserAccess);
+ }
+
+ public function test_getSuperUserAccessFromSuperUserAttribute_ReturnsFalse_IfInstanceNotInAttribute()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("whatever");
+ $this->assertFalse($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("one;two;three");
+ $this->assertFalse($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("one:two:three");
+ $this->assertFalse($hasSuperUserAccess);
+
+ $this->userAccessAttributeParser->setThisPiwikInstanceName(null);
+ $this->userAccessAttributeParser->setServerIdsSeparator('|');
+ $this->setThisPiwikUrl("https://whatever.com/piwik");
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("https://whatever.com:8080/piwik");
+ $this->assertFalse($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("https://whatever.com:8080");
+ $this->assertFalse($hasSuperUserAccess);
+ }
+
+ public function test_getSuperUserAccessFromSuperUserAttribute_ReturnsCorrectResult_IfAttributeValueIsMalformed()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute(" myPiwik = superuser ; whatever");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("anothe; superuser = myPiwik ; whatever");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $this->userAccessAttributeParser->setThisPiwikInstanceName(null);
+ $this->userAccessAttributeParser->setServerSpecificationDelimiter('|');
+ $this->setThisPiwikUrl("https://whatever.com/piwik");
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("whatever.com/piwik | @{}{}@ | /&/?//");
+ $this->assertTrue($hasSuperUserAccess);
+ }
+
+ public function test_getSuperUserAccessFromSuperUserAttribute_ReturnsCorrectResult_WhenCustomDelimetersAreUsed()
+ {
+ $this->userAccessAttributeParser->setThisPiwikInstanceName('myPiwik');
+ $this->userAccessAttributeParser->setServerSpecificationDelimiter('#');
+ $this->userAccessAttributeParser->setServerIdsSeparator('|');
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("anoth | myPiwik | whatever");
+ $this->assertTrue($hasSuperUserAccess);
+
+ $hasSuperUserAccess = $this->userAccessAttributeParser->getSuperUserAccessFromSuperUserAttribute("a # myPiwik # c");
+ $this->assertTrue($hasSuperUserAccess);
+ }
+
+ private function setSitesManagerApiMock()
+ {
+ $mock = $this->getMockBuilder('stdClass')
+ ->setMethods( array('getSitesIdWithAtLeastViewAccess', 'getAllSitesId'))
+ ->getMock();
+ $mock->expects($this->any())->method('getSitesIdWithAtLeastViewAccess')->willReturn(array(1,2,3,4,5,6));
+ $mock->expects($this->any())->method('getAllSitesId')->willReturn(array(1,2,3,4,5,6));
+ SitesManagerAPI::setSingletonInstance($mock);
+ }
+
+ private function setThisPiwikUrl($thisUrl)
+ {
+ $mock = $this->getMockBuilder('stdClass')
+ ->setMethods(array('getValue'))
+ ->getMock();
+ $mock->expects($this->any())->method('getValue')->willReturnCallback(function ($key) use ($thisUrl) {
+ if ($key == SettingsPiwik::OPTION_PIWIK_URL) {
+ return $thisUrl;
+ } else {
+ return "...";
+ }
+ });
+
+ Option::setSingletonInstance($mock);
+ }
+}
diff --git a/tests/Unit/UserAccessMapperTest.php b/tests/Unit/UserAccessMapperTest.php
new file mode 100644
index 0000000..d2471e6
--- /dev/null
+++ b/tests/Unit/UserAccessMapperTest.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use PHPUnit_Framework_TestCase;
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserAccessAttributeParser;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserAccessMapper;
+use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_UserAccessMapperTest
+ */
+class UserAccessMapperTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @var UserAccessMapper
+ */
+ private $userAccessMapper;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->LoginLdap = array();
+
+ $this->setSitesManagerApiMock();
+
+ $this->userAccessMapper = new UserAccessMapper();
+
+ $attributeParser = new UserAccessAttributeParser();
+ $attributeParser->setThisPiwikInstanceName('thisPiwik');
+ $this->userAccessMapper->setUserAccessAttributeParser($attributeParser);
+ }
+
+ public function tearDown()
+ {
+ SitesManagerAPI::unsetInstance();
+ }
+
+ public function test_makeConfigured_CreatesCorrectlyConfiguredInstance_WhenAllConfigSupplied()
+ {
+ Config::getInstance()->LoginLdap = array(
+ 'ldap_view_access_field' => 'viewaccessfield',
+ 'ldap_admin_access_field' => 'adminaccessfield',
+ 'ldap_superuser_access_field' => 'superuseraccessfield'
+ );
+
+ $userAccessMapper = UserAccessMapper::makeConfigured();
+ $this->assertEquals('viewaccessfield', $userAccessMapper->getViewAttributeName());
+ $this->assertEquals('adminaccessfield', $userAccessMapper->getAdminAttributeName());
+ $this->assertEquals('superuseraccessfield', $userAccessMapper->getSuperuserAttributeName());
+ }
+
+ public function test_makeConfigured_CreatesCorrectlyConfiguredInstance_WhenNoConfigOptionsPresent()
+ {
+ $userAccessMapper = UserAccessMapper::makeConfigured();
+ $this->assertEquals('view', $userAccessMapper->getViewAttributeName());
+ $this->assertEquals('admin', $userAccessMapper->getAdminAttributeName());
+ $this->assertEquals('superuser', $userAccessMapper->getSuperuserAttributeName());
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_CorrectlyMapsAccess_WhenUserIsSuperUser()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'superuser' => '1'
+ ));
+
+ $this->checkSuperUserAccess($access);
+
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'admin' => '1,2,3',
+ 'view' => '3,4,5',
+ 'superuser' => '1'
+ ));
+
+ $this->checkSuperUserAccess($access);
+
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'superuser' => null
+ ));
+
+ $this->checkSuperUserAccess($access);
+
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'superuser' => array()
+ ));
+
+ $this->assertEquals(array(), $access);
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_CorrectlyMapsAccess_WhenUserHasViewAndAdminAccess()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'view' => '3,4',
+ 'admin' => '1,2'
+ ));
+
+ $expectedAccess = array(
+ 'admin' => array(1,2),
+ 'view' => array(3,4)
+ );
+ $this->assertEquals($expectedAccess, $access);
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_UsesHighestAccessLevel_WhenUserHasViewAndAdminAccessForSameSite()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'view' => '1,4',
+ 'admin' => '1,2'
+ ));
+
+ $expectedAccess = array(
+ 'admin' => array(1,2),
+ 'view' => array(4)
+ );
+ $this->assertEquals($expectedAccess, $access);
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_CorrectlyMapsAccess_WhenLdapAttributesAreArrays()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'view' => array('3,5,6', 4),
+ 'admin' => array(1,'2')
+ ));
+
+ $expectedAccess = array(
+ 'admin' => array(1,2),
+ 'view' => array(3,5,6,4)
+ );
+ $this->assertEquals($expectedAccess, $access);
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_CorrectlyMapsAccess_WhenLdapAttributeIsAllString()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'view' => 'all',
+ 'admin' => 'all'
+ ));
+
+ $expectedAccess = array(
+ 'admin' => array(1,2,3,4,5,6)
+ );
+ $this->assertEquals($expectedAccess, $access);
+ }
+
+ public function test_getPiwikUserAccessForLdapUser_IgnoresSitesThatDoNotExist()
+ {
+ $access = $this->userAccessMapper->getPiwikUserAccessForLdapUser(array(
+ 'view' => array(15,16,'17,18'),
+ 'admin' => '11,12,13'
+ ));
+
+ $expectedAccess = array();
+ $this->assertEquals($expectedAccess, $access);
+ }
+
+ private function checkSuperUserAccess($access)
+ {
+ $this->assertEquals(array('superuser' => true), $access);
+ }
+
+ private function setSitesManagerApiMock()
+ {
+ $mock = $this->getMockBuilder('stdClass')
+ ->setMethods(array('getSitesIdWithAtLeastViewAccess', 'getAllSitesId'))
+ ->getMock();
+ $mock->expects($this->any())->method('getSitesIdWithAtLeastViewAccess')->willReturn(array(1,2,3,4,5,6));
+ $mock->expects($this->any())->method('getAllSitesId')->willReturn(array(1,2,3,4,5,6));
+ SitesManagerAPI::setSingletonInstance($mock);
+ }
+}
diff --git a/tests/Unit/UserMapperTest.php b/tests/Unit/UserMapperTest.php
new file mode 100644
index 0000000..20b24b3
--- /dev/null
+++ b/tests/Unit/UserMapperTest.php
@@ -0,0 +1,292 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use Exception;
+use PHPUnit_Framework_TestCase;
+use Piwik\Auth\Password;
+use Piwik\Config;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserMapper;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_UserMapperTest
+ */
+class UserMapperTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @var UserMapper
+ */
+ private $userMapper;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->userMapper = new UserMapper();
+ }
+
+ public function test_makeConfigured_CreatesCorrectUserMapper_WhenAllConfigOptionsSupplied()
+ {
+ Config::getInstance()->LoginLdap = array(
+ 'ldap_user_id_field' => 'userIdField',
+ 'ldap_last_name_field' => 'lastNameField',
+ 'ldap_first_name_field' => 'firstNameField',
+ 'ldap_alias_field' => 'aliasField',
+ 'ldap_mail_field' => 'mailField',
+ 'ldap_password_field' => 'passwordField',
+ 'user_email_suffix' => 'userEmailSuffix',
+ );
+
+ $userMapper = UserMapper::makeConfigured();
+
+ $this->assertUserMapperIsCorrectlyConfigured($userMapper);
+ }
+
+ public function test_makeConfigured_CreatesCorrectUserMapper_WhenOldConfigNamesUsed()
+ {
+ Config::getInstance()->LoginLdap = array(
+ 'userIdField' => 'userIdField',
+ 'ldap_last_name_field' => 'lastNameField',
+ 'ldap_first_name_field' => 'firstNameField',
+ 'aliasField' => 'aliasField',
+ 'mailField' => 'mailField',
+ 'ldap_password_field' => 'passwordField',
+ 'usernameSuffix' => 'userEmailSuffix',
+ );
+
+ $userMapper = UserMapper::makeConfigured();
+
+ $this->assertUserMapperIsCorrectlyConfigured($userMapper);
+ }
+
+ public function test_makeConfigured_UsesCorrectDefaultValues()
+ {
+ Config::getInstance()->LoginLdap = array();
+
+ $userMapper = UserMapper::makeConfigured();
+
+ $this->assertUserMapperHasCorrectDefaultPropertyValues($userMapper);
+ }
+
+ public function test_createPiwikUserFromLdapUser_CreatesCorrectPiwikUser_WhenAllLdapUserFieldsArePresent()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'martha',
+ 'cn' => 'A real doctor',
+ 'sn' => 'Jones',
+ 'givenname' => 'Martha',
+ 'mail' => 'martha@unit.co.uk',
+ 'userpassword' => 'pass',
+ 'other' => 'sfdklsdjf'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'martha',
+ 'password' => md5('pass'),
+ 'email' => 'martha@unit.co.uk',
+ 'alias' => 'A real doctor'
+ ), $result);
+ }
+
+ public function test_createPiwikUserFromLdapUser_CreatesCorrectPiwikUser_WhenCustomLdapAttributesAreUsedAndPresent()
+ {
+ $this->userMapper->setLdapAliasField('testfield1');
+ $this->userMapper->setLdapUserIdField('testfield2');
+ $this->userMapper->setLdapMailField('testfield3');
+ $this->userMapper->setLdapFirstNameField('testfield4');
+ $this->userMapper->setLdapLastNameField('testfield5');
+ $this->userMapper->setLdapUserPasswordField('testfield6');
+
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'testfield1' => 'am i bovvered?',
+ 'testfield2' => 'donna',
+ 'testfield3' => 'donna@rstad.com',
+ 'testfield4' => 'Donna',
+ 'testfield5' => 'Noble',
+ 'testfield6' => 'pass',
+ 'other3' => 'sdlfdsf'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'donna',
+ 'password' => md5('pass'),
+ 'email' => 'donna@rstad.com',
+ 'alias' => 'am i bovvered?'
+ ), $result);
+
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'testfield2' => 'donna',
+ 'testfield3' => 'donna@rstad.com',
+ 'testfield4' => 'Donna',
+ 'testfield5' => 'Noble',
+ 'testfield6' => 'pass',
+ 'other3' => 'sdlfdsf'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'donna',
+ 'password' => md5('pass'),
+ 'email' => 'donna@rstad.com',
+ 'alias' => 'Donna Noble'
+ ), $result);
+ }
+
+ /**
+ * @expectedException Exception
+ */
+ public function test_createPiwikUserFromLdapUser_FailsToCreatePiwikUser_WhenUIDAttributeIsMissing()
+ {
+ $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'cn' => 'the impossible girl',
+ 'sn' => 'Oswald',
+ 'givenname' => 'Clara',
+ 'mail' => 'clara@coalhill.co.uk',
+ 'userpassword' => 'pass'
+ ));
+ }
+
+ public function test_createPiwikUserFromLdapUser_CreatesPiwikUser_WhenAliasAndNamesAreMissing()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'clara',
+ 'mail' => 'clara@coalhill.co.uk',
+ 'userpassword' => 'pass'
+ ));
+
+ $this->assertEmpty($result['alias']);
+ }
+
+ public function test_createPiwikUserFromLdapUser_CreatesPiwikUserWithRandomPassword_WhenUserPasswordIsMissing()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'clara',
+ 'sn' => 'Oswald',
+ 'givenname' => 'Clara',
+ 'mail' => 'clara@coalhill.co.uk'
+ ));
+
+ $this->assertNotEmpty($result['password']);
+ }
+
+ public function test_createPiwikUserFromLdapUser_SetsCorrectEmail_WhenUserHasNone()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'pond',
+ 'cn' => 'kissogram',
+ 'userpassword' => 'pass'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'pond',
+ 'password' => md5('pass'),
+ 'email' => 'pond@mydomain.com',
+ 'alias' => 'kissogram'
+ ), $result);
+
+ $this->userMapper->setUserEmailSuffix('@royalleadworthhospital.co.uk');
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'mrpond',
+ 'cn' => 'not quite Bond',
+ 'userpassword' => 'pass'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'mrpond',
+ 'password' => md5('pass'),
+ 'email' => 'mrpond@royalleadworthhospital.co.uk',
+ 'alias' => 'not quite Bond'
+ ), $result);
+ }
+
+ public function test_createPiwikUserEntryForLdapUser_SetsCorrectAlias_WhenUserHasFirstAndLastName()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'harkness',
+ 'sn' => 'Harkness',
+ 'givenname' => 'Captain',
+ 'userpassword' => 'pass',
+ 'other' => 'sfdklsdjf'
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'harkness',
+ 'password' => md5('pass'),
+ 'email' => 'harkness@mydomain.com',
+ 'alias' => 'Captain Harkness'
+ ), $result);
+ }
+
+ public function test_createPiwikUserEntryForLdapUser_CreatesCorrectPiwikUser_IfLdapUserInfoIsAnArray()
+ {
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => array('rose'),
+ 'cn' => array('bad wolf'),
+ 'sn' => array('Tyler'),
+ 'givenname' => array('Rose'),
+ 'mail' => array('rose@linda.com'),
+ 'userpassword' => array('pass'),
+ 'other' => array('sfdklsdjf)')
+ ));
+
+ $this->assertEquals(array(
+ 'login' => 'rose',
+ 'password' => md5('pass'),
+ 'email' => 'rose@linda.com',
+ 'alias' => 'bad wolf'
+ ), $result);
+ }
+
+ public function test_createPiwikUserEntryForLdapUser_UsesExistingPassword()
+ {
+ $existingUser = array(
+ 'login' => 'broken',
+ 'alias' => 'alias',
+ 'email' => 'wrongmail',
+ 'password' => 'existingpass'
+ );
+
+ $result = $this->userMapper->createPiwikUserFromLdapUser(array(
+ 'uid' => 'leela',
+ 'cn' => 'Leela of the Sevateem',
+ 'mail' => 'leela@gallifrey.???',
+ 'userpassword' => 'pass'
+ ), $existingUser);
+
+ $this->assertEquals(array(
+ 'login' => 'leela',
+ 'alias' => 'Leela of the Sevateem',
+ 'password' => 'existingpass',
+ 'email' => 'leela@gallifrey.???'
+ ), $result);
+ }
+
+ private function assertUserMapperIsCorrectlyConfigured(UserMapper $userMapper)
+ {
+ $this->assertEquals('useridfield', $userMapper->getLdapUserIdField());
+ $this->assertEquals('lastnamefield', $userMapper->getLdapLastNameField());
+ $this->assertEquals('firstnamefield', $userMapper->getLdapFirstNameField());
+ $this->assertEquals('aliasfield', $userMapper->getLdapAliasField());
+ $this->assertEquals('mailfield', $userMapper->getLdapMailField());
+ $this->assertEquals('passwordfield', $userMapper->getLdapUserPasswordField());
+ $this->assertEquals('userEmailSuffix', $userMapper->getUserEmailSuffix());
+ }
+
+ private function assertUserMapperHasCorrectDefaultPropertyValues(UserMapper $userMapper)
+ {
+ $this->assertEquals('uid', $userMapper->getLdapUserIdField());
+ $this->assertEquals('sn', $userMapper->getLdapLastNameField());
+ $this->assertEquals('givenname', $userMapper->getLdapFirstNameField());
+ $this->assertEquals('cn', $userMapper->getLdapAliasField());
+ $this->assertEquals('mail', $userMapper->getLdapMailField());
+ $this->assertEquals('userpassword', $userMapper->getLdapUserPasswordField());
+ $this->assertEquals('@mydomain.com', $userMapper->getUserEmailSuffix());
+ }
+} \ No newline at end of file
diff --git a/tests/Unit/UserSynchronizerTest.php b/tests/Unit/UserSynchronizerTest.php
new file mode 100644
index 0000000..3fdea2a
--- /dev/null
+++ b/tests/Unit/UserSynchronizerTest.php
@@ -0,0 +1,228 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginLdap\tests\Unit;
+
+use Exception;
+use PHPUnit_Framework_TestCase;
+use Piwik\Access;
+use Piwik\Auth\Password;
+use Piwik\Config;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\LoginLdap\LdapInterop\UserSynchronizer;
+use Piwik\Plugins\UsersManager\Model;
+use Piwik\Plugins\UsersManager\UserAccessFilter;
+
+/**
+ * @group LoginLdap
+ * @group LoginLdap_Unit
+ * @group LoginLdap_UserSynchronizerTest
+ */
+class UserSynchronizerTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @var UserSynchronizer
+ */
+ private $userSynchronizer;
+
+ /**
+ * @var array
+ */
+ public $userAccess;
+
+ /**
+ * @var array
+ */
+ public $superUserAccess;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->userSynchronizer = new UserSynchronizer();
+ $this->userSynchronizer->setNewUserDefaultSitesWithViewAccess(array(1,2));
+ $this->setUserModelMock($this->getPiwikUserData());
+ $this->setUserMapperMock($this->getPiwikUserData());
+
+ $this->userAccess = array();
+ $this->superUserAccess = array();
+ }
+
+ public function test_makeConfigured_DoesNotThrow_WhenUserMapperCorrectlyConfigured()
+ {
+ Config::getInstance()->LoginLdap = array(
+ 'ldap_user_id_field' => 'userIdField',
+ 'ldap_last_name_field' => 'lastNameField',
+ 'ldap_first_name_field' => 'firstNameField',
+ 'ldap_alias_field' => 'aliasField',
+ 'ldap_mail_field' => 'mailField',
+ 'user_email_suffix' => 'userEmailSuffix',
+ );
+
+ $result = UserSynchronizer::makeConfigured();
+ $this->assertNotNull($result);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage dummy
+ */
+ public function test_synchronizeLdapUser_Throws_IfUserMapperCannotCorrectlyCreatePiwikUser()
+ {
+ $this->setUserMapperMock($value = null, $throws = true);
+
+ $this->userSynchronizer->synchronizeLdapUser('piwikuser', array());
+ }
+
+ public function test_synchronizeLdapUser_ReturnsUserManagerApiResultWithoutPassword()
+ {
+ $this->setUserManagerApiMock($throws = false);
+ $this->setUserModelMock(null);
+
+ $result = $this->userSynchronizer->synchronizeLdapUser('piwikuser', array());
+
+ $this->assertTrue(empty($result['password']), "Password set in synchronizeLdapUser result, it shouldn't be.");
+ $this->assertEquals(array(
+ array('piwikuser', 'view', array(1,2))
+ ), $this->userAccess);
+ }
+
+ /**
+ * @expectedException Exception
+ */
+ public function test_synchronizeLdapUser_Throws_IfUserManagerApiThrows()
+ {
+ $this->setUserManagerApiMock($throwsInAddUser = true, $throwsInUpdateUser = true);
+ $this->setUserModelMock(null);
+
+ $this->userSynchronizer->synchronizeLdapUser('piwikuser', array());
+ }
+
+ public function test_synchronizeLdapUser_Succeeds_IfUserDoesNotExistInDb()
+ {
+ $this->setUserManagerApiMock($throws = false);
+ $this->setUserModelMock(null);
+
+ $this->userSynchronizer->synchronizeLdapUser('piwikuser', array());
+ }
+
+ public function test_synchronizePiwikAccessFromLdap_DoesNotSynchronizeUserAccessOnUpdate_WhenUserAccessMapperNotUsed()
+ {
+ $this->setUserManagerApiMock($throwsOnAdd = false, $throwsOnUpdate = false, $throwsOnSetAccess = true);
+
+ $this->userSynchronizer->synchronizePiwikAccessFromLdap('piwikuser', array());
+ }
+
+ public function test_synchronizePiwikAccessFromLdap_WillSetAccessCorrectly()
+ {
+ $this->setUserManagerApiMock($throwsOnAdd = false);
+ $this->setUserAccessMapperMock(array(
+ 'superuser' => array(7,8,9),
+ 'view' => array(1,2,3),
+ 'admin' => array(4,5,6)
+ ));
+
+ $this->userSynchronizer->synchronizePiwikAccessFromLdap('piwikuser', array());
+
+ $this->assertEquals(array(
+ array('piwikuser', 'view', array(1,2,3)),
+ array('piwikuser', 'admin', array(4,5,6))
+ ), $this->userAccess);
+
+ $this->assertEquals(array(
+ array('piwikuser', true)
+ ), $this->superUserAccess);
+ }
+
+ public function test_synchronizePiwikAccessFromLdap_Succeeds_IfLdapUserHasNoAccess()
+ {
+ $this->setUserManagerApiMock($throwsOnAdd = false);
+ $this->setUserAccessMapperMock(array());
+
+ $this->userSynchronizer->synchronizePiwikAccessFromLdap('piwikuser', array());
+ $this->assertEquals(array(), $this->userAccess);
+ $this->assertEquals(array(), $this->superUserAccess);
+ }
+
+ private function setUserManagerApiMock($throwsOnAddUser, $throwsOnUpdateUser = false, $throwsOnSetAccess = false)
+ {
+ $self = $this;
+ $model = new Model();
+
+ $mock = $this->getMockBuilder('Piwik\Plugins\UsersManager\API')
+ ->setMethods(array('addUser', 'updateUser', 'getUser', 'setUserAccess', 'setSuperUserAccess'))
+ ->setConstructorArgs(array($model, new UserAccessFilter($model, new Access()), new Password()))
+ ->getMock();
+ if ($throwsOnAddUser) {
+ $mock->expects($this->any())->method('addUser')->willThrowException(new Exception("dummy message"));
+ } else {
+ $mock->expects($this->any())->method('addUser');
+ }
+ if ($throwsOnUpdateUser) {
+ $mock->expects($this->any())->method('updateUser')->willThrowException(new Exception("dummy message"));
+ } else {
+ $mock->expects($this->any())->method('updateUser');
+ }
+
+ if ($throwsOnSetAccess) {
+ $mock->expects($this->any())->method('setUserAccess')->willThrowException(new Exception("dummy message"));
+ } else {
+ $mock->expects($this->any())->method('setUserAccess')->willReturnCallback(function ($login, $access, $sites) use ($self) {
+ $self->userAccess[] = array($login, $access, $sites);
+ });
+ }
+
+ $mock->expects($this->any())->method('setSuperUserAccess')->willReturnCallback(function ($login, $hasSuperUserAccess) use ($self) {
+ $self->superUserAccess[] = array($login, $hasSuperUserAccess);
+ });
+
+ $this->userSynchronizer->setUsersManagerApi($mock);
+ }
+
+ private function getPiwikUserData()
+ {
+ return array(
+ 'login' => 'piwikuser',
+ 'password' => 'password',
+ 'email' => 'email',
+ 'alias' => 'alias'
+ );
+ }
+
+ private function setUserMapperMock($value, $throws = false)
+ {
+ $mock = $this->getMockBuilder('Piwik\Plugins\LoginLdap\LdapInterop\UserMapper')
+ ->setMethods(array('createPiwikUserFromLdapUser', 'markUserAsLdapUser'))
+ ->getMock();
+ if ($throws) {
+ $mock->expects($this->any())->method('createPiwikUserFromLdapUser')->will($this->throwException(new Exception("dummy")));
+ } else {
+ $mock->expects($this->any())->method('createPiwikUserFromLdapUser')->will($this->returnValue($value));
+ }
+ $this->userSynchronizer->setUserMapper($mock);
+ }
+
+ private function setUserAccessMapperMock($value)
+ {
+ $mock = $this->getMockBuilder('Piwik\Plugins\LoginLdap\LdapInterop\UserAccessMapper')
+ ->setMethods( array('getPiwikUserAccessForLdapUser'))
+ ->getMock();
+ $mock->expects($this->any())->method('getPiwikUserAccessForLdapUser')->will($this->returnValue($value));
+ $this->userSynchronizer->setUserAccessMapper($mock);
+ }
+
+ private function setUserModelMock($returnValue)
+ {
+ $mock = $this->getMockBuilder('Piwik\Plugins\UsersManager\Model')
+ ->setMethods(array('getUser', 'deleteUserAccess', 'setSuperUserAccess'))
+ ->getMock();
+ $mock->expects($this->any())->method('getUser')->will($this->returnValue($returnValue));
+
+ $this->userSynchronizer->setUserModel($mock);
+ }
+}
diff --git a/tests/travis/after_script.after.yml b/tests/travis/after_script.after.yml
new file mode 100644
index 0000000..241e7f9
--- /dev/null
+++ b/tests/travis/after_script.after.yml
@@ -0,0 +1 @@
+ - sudo grep slapd /var/log/syslog \ No newline at end of file
diff --git a/tests/travis/install.after.yml b/tests/travis/install.after.yml
new file mode 100644
index 0000000..4fd52a3
--- /dev/null
+++ b/tests/travis/install.after.yml
@@ -0,0 +1 @@
+ - ./plugins/LoginLdap/tests/travis/setup_ldap.sh
diff --git a/tests/travis/setup_ldap.sh b/tests/travis/setup_ldap.sh
new file mode 100755
index 0000000..46640b7
--- /dev/null
+++ b/tests/travis/setup_ldap.sh
@@ -0,0 +1,305 @@
+#!/bin/bash
+
+# install LDAP
+echo "Installing LDAP..."
+sudo apt-get update > /dev/null
+if ! sudo apt-get install slapd ldap-utils -y -qq > /dev/null; then
+ echo "Failed to install OpenLDAP!"
+fi
+
+# configure LDAP
+echo ""
+echo "Configuring LDAP..."
+
+mkdir -p /tmp/ldap
+sudo chmod -R 777 /tmp/ldap
+
+ADMIN_USER=fury
+ADMIN_PASS=secrets
+ADMIN_PASS_HASH=`slappasswd -h {md5} -s $ADMIN_PASS`
+BASE_DN="dc=avengers,dc=shield,dc=org"
+
+STR_OID="1.3.6.1.4.1.1466.115.121.1.15"
+VIEW_OID="2.16.840.1.113730.3.1.1.1"
+ADMIN_OID="2.16.840.1.113730.3.1.1.2"
+SUPERUSER_OID="2.16.840.1.113730.3.1.1.3"
+
+sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
+
+dn: cn=config
+changetype: modify
+replace: olcLogLevel
+olcLogLevel: -1
+-
+add: olcDisallows
+olcDisallows: bind_anon
+
+EOF
+
+if [ "$?" -ne "0" ]; then
+ echo "Failed to change config olcLogLevel or olcDisallows!"
+ echo ""
+ echo "slapd log:"
+ sudo grep slapd /var/log/syslog
+
+ exit 1
+fi
+
+sudo ldapadd -Y EXTERNAL -H ldapi:/// <<EOF
+
+# database
+dn: olcDatabase={2}hdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcHdbConfig
+olcDatabase: {2}hdb
+olcRootDN: cn=$ADMIN_USER,$BASE_DN
+olcRootPW: $ADMIN_PASS_HASH
+olcDbDirectory: /tmp/ldap
+olcSuffix: $BASE_DN
+olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by dn="cn=$ADMIN_USER,$BASE_DN" write by * auth
+olcAccess: {1}to dn.base="" by dn="cn=$ADMIN_USER,$BASE_DN" write by * read
+olcAccess: {2}to * by self write by dn="cn=$ADMIN_USER,$BASE_DN" write by * read
+olcRequires: authc
+olcLastMod: TRUE
+olcDbCheckpoint: 512 30
+olcDbConfig: {0}set_cachesize 0 2097152 0
+olcDbConfig: {1}set_lk_max_objects 1500
+olcDbConfig: {2}set_lk_max_locks 1500
+olcDbConfig: {3}set_lk_max_lockers 1500
+olcDbIndex: objectClass eq
+
+# modules
+dn: cn=module,cn=config
+cn: module
+objectClass: olcModuleList
+objectClass: top
+olcModulePath: /usr/lib/ldap
+olcModuleLoad: memberof.la
+
+dn: olcOverlay={0}memberof,olcDatabase={2}hdb,cn=config
+objectClass: olcConfig
+objectClass: olcMemberOf
+objectClass: olcOverlayConfig
+objectClass: top
+olcOverlay: memberof
+
+dn: cn=module,cn=config
+cn: module
+objectclass: olcModuleList
+objectClass: top
+olcModuleLoad: refint.la
+olcModulePath: /usr/lib/ldap
+
+dn: olcOverlay={1}refint,olcDatabase={2}hdb,cn=config
+objectClass: olcConfig
+objectClass: olcOverlayConfig
+objectClass: olcRefintConfig
+objectClass: top
+olcOverlay: {1}refint
+olcRefintAttribute: memberof member manager owner
+
+EOF
+
+if [ "$?" -ne "0" ]; then
+ echo "Failed to change config database or modules!"
+ echo ""
+ echo "slapd log:"
+ sudo grep slapd /var/log/syslog
+
+ exit 1
+fi
+
+sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
+
+# first define custom LDAP attributes for Piwik access
+dn: cn=schema,cn=config
+changetype: modify
+add: olcAttributeTypes
+olcAttributeTypes: ( $VIEW_OID
+ NAME 'view'
+ DESC 'Describes site IDs user has view access to.'
+ EQUALITY caseIgnoreMatch
+ ORDERING caseIgnoreOrderingMatch
+ SYNTAX $STR_OID )
+-
+add: olcAttributeTypes
+olcAttributeTypes: ( $ADMIN_OID
+ NAME 'admin'
+ DESC 'Describes site IDs user has admin access to.'
+ EQUALITY caseIgnoreMatch
+ ORDERING caseIgnoreOrderingMatch
+ SYNTAX $STR_OID )
+-
+add: olcAttributeTypes
+olcAttributeTypes: ( $SUPERUSER_OID
+ NAME 'superuser'
+ DESC 'Marks user as superuser if present.'
+ EQUALITY caseIgnoreMatch
+ ORDERING caseIgnoreOrderingMatch
+ SYNTAX $STR_OID )
+
+EOF
+
+if [ "$?" -ne "0" ]; then
+ echo "Failed to add custom attributes!"
+ echo ""
+ echo "slapd log:"
+ sudo grep slapd /var/log/syslog
+
+ exit 1
+fi
+
+sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
+
+dn: cn=schema,cn=config
+changetype: modify
+add: olcObjectClasses
+olcObjectClasses: ( 2.16.840.1.113730.3.2.3
+ NAME 'piwikPerson'
+ DESC 'Piwik User'
+ SUP inetOrgPerson
+ STRUCTURAL
+ MAY ( view $ admin $ superuser )
+ )
+
+EOF
+
+if [ "$?" -ne "0" ]; then
+ echo "Failed to add piwikPerson class!"
+ echo ""
+ echo "slapd log:"
+ sudo grep slapd /var/log/syslog
+
+ exit 1
+fi
+
+echo "Configured."
+
+# add entries to LDAP
+echo ""
+echo "Adding entries to LDAP..."
+
+sudo ldapadd -xv -w $ADMIN_PASS -D cn=$ADMIN_USER,$BASE_DN <<EOF
+
+# base dn
+dn: $BASE_DN
+objectClass: domain
+objectClass: top
+dc: avengers
+
+# ou entry
+dn: ou=Groups,$BASE_DN
+objectclass: organizationalunit
+ou: Groups
+description: all groups
+
+# USER ENTRY (pwd: piedpiper)
+dn: cn=Tony Stark,$BASE_DN
+cn: Tony Stark
+sn: Stark
+givenName: Tony
+objectClass: piwikPerson
+objectClass: top
+uid: ironman
+userPassword: `slappasswd -h {md5} -s piedpiper`
+mobile: 555-555-5555
+mail: billionairephilanthropistplayboy@starkindustries.com
+view: 1,2
+view: 3
+admin: 3
+
+# USER ENTRY (pwd: redledger)
+dn: cn=Natalia Romanova,$BASE_DN
+cn: Natalia Romanova
+objectClass: top
+objectClass: piwikPerson
+sn: Romanova
+givenName: Natalia
+uid: blackwidow
+userPassword: `slappasswd -h {md5} -s redledger`
+mobile: none
+view: myPiwik:1,2;anotherPiwik:3,4
+admin: myPiwik:3,4
+admin: anotherPiwik:5,6
+
+# USER ENTRY (pwd: thaifood)
+dn: cn=Steve Rodgers,$BASE_DN
+cn: Steve Rodgers
+objectClass: top
+objectClass: piwikPerson
+sn: Rodgers
+givenName: Steve
+uid: captainamerica
+userPassword: `slappasswd -h {md5} -s thaifood`
+mobile: 123-456-7890
+mail: srodgers@aol.com
+superuser: 1
+superuser: anotherPiwik
+
+# USER ENTRY (pwd: bilgesnipe)
+dn: cn=Thor,$BASE_DN
+cn: Thor
+objectClass: top
+objectClass: piwikPerson
+sn: Odinson
+givenName: Thor
+uid: thor
+userPassword: `slappasswd -h {md5} -s bilgesnipe`
+view: localhost:1,2;whatever.com:3,4
+admin: whatever.com:1,2
+admin: localhost:3,4
+superuser: myPiwik:myOtherPiwik;localhost
+
+# USER ENTRY (pwd: enrogue)
+dn: cn=Ms Marvel,$BASE_DN
+objectClass: top
+objectClass: piwikPerson
+cn: Ms Marvel
+uid: msmarvel
+userPassword: `slappasswd -h {md5} -s enrogue`
+sn: Danvers
+
+# group entry
+dn: cn=avengers,$BASE_DN
+cn: avengers
+objectClass: groupOfNames
+objectClass: top
+member: cn=Tony Stark,$BASE_DN
+member: cn=Natalia Romanova,$BASE_DN
+member: cn=Steve Rodgers,$BASE_DN
+member: cn=Thor,$BASE_DN
+
+# another group entry
+dn: cn=S.H.I.E.L.D.,$BASE_DN
+cn: S.H.I.E.L.D.
+objectClass: groupOfNames
+objectClass: top
+member: cn=Natalia Romanova,$BASE_DN
+
+# USER ENTRY (pwd: cher)
+dn: cn=Rogue,$BASE_DN
+objectClass: top
+objectClass: piwikPerson
+cn: Rogue
+uid: rogue@xmansion.org
+userPassword: `slappasswd -h {md5} -s cher`
+sn: Doesnthaveone
+
+EOF
+
+if [ "$?" -eq "0" ]; then
+ echo "Added entries."
+else
+ echo "Failed to add entries."
+ echo ""
+ echo "slapd log:"
+ sudo grep slapd /var/log/syslog
+
+ exit 1
+fi
+
+echo ldapsearch -x -D "cn=Tony Stark,$BASE_DN" -w "piedpiper" -b "$BASE_DN" "(uid=ironman)" memberOf
+ldapsearch -x -D "cn=Tony Stark,$BASE_DN" -w "piedpiper" -b "$BASE_DN" "(uid=ironman)" memberOf
+
+echo ldapsearch -x -D "cn=$ADMIN_USER,$BASE_DN" -w "$ADMIN_PASS" -b "$BASE_DN"
+ldapsearch -x -D "cn=$ADMIN_USER,$BASE_DN" -w "$ADMIN_PASS" -b "$BASE_DN"