<?php
/**
 * See LICENSE.md distributed with the software package for license informations.
 *
 * THANKS TO:
 * - Fedek. He wrote a similar plugin for yii 1.
 * - stevebauman. He is maintaining the Adldap2 repository on github.com
 *
 * @category yii2-extension
 * @package yii2-adldap-module
 * @author Matthias Maderer
 * @copyright (c) 2017 Matthias Maderer
 * @link https://github.com/edvler/yii2-adldap-module
 */

namespace Edvlerblog\Adldap2\model;

use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
use Adldap\Models\Attributes\AccountControl;
use Adldap\Utilities;

/**
 * User model with database integration and LDAP synchronistation.
 *
 * @property integer $id
 * @property string $username
 * @property integer $status
 * @property string $auth_key
 * @property integer $created_at
 * @property integer $updated_at
 */
class UserDbLdap extends ActiveRecord implements IdentityInterface
{
    /**
     * Constants starting with SYNC_OPTIONS_****
     * This constant defines all options needed, that are belogn to the login behavior
     * of the UserDbLdap.
     *
     * The main purpose of this constant is to define when a LDAP query is done and when not.
     * For example you can decide, if a user is not found in the yii2 database, if a LDAP query should search
     * for the user and create it, or if the login simply fails.
     * Another example is, if a LDAP query should be issued on login to refresh the group assignments.
     *
     * As you can imagine updating group assignments and updating the account status could be a
     * time consuming task. This is why you can define what should be done on every site request
     * and on login.
     *
     * If you run a backend task, which querys the informations
     * from active directory every X minutes with cron or a another scheduler,
     * you can completly deactivate all refreshs on login. See constant SYNC_OPTIONS_TEMPLATE_ONLY_BACKEND_TASK
     * for example.
     *
     * You can configure your own settings in the config/params.php too.
     *
     *   With predefined constant
     *
     *   return [
     *       //...
     *       'LDAP-User-Sync-Options' => Edvlerblog\Adldap2\model\UserDbLdap::SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK,
     *       'LDAP-Group-Assignment-Options' => Edvlerblog\Adldap2\model\UserDbLdap::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX,
     *       //...
     *   ];
     *
     *   With complete own settings
     *
     *   return [
     *       //...
     *       'LDAP-User-Sync-Options' => [
     *                                       'ON_LOGIN_CREATE_USER' => false,
     *                                       'ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS' => false,
     *                                       'ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS' => true,
     *                                       'ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS' => false,
     *                                   ],
     *       'LDAP-Group-Assignment-Options' => Edvlerblog\Adldap2\model\UserDbLdap::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX,
     *       //...
     *   ];
     *
     */

    /**
     * Constant SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK
     *
     * This constant is DEFAULT if you doesn't configure anything.
     * - On login a user is automatically created, if it not exists in database
     * - On login the group assignments are refreshed
     * - On login the account status is refreshed
     *
     * In simple words:
     * If you login with a Active Directory user, which is active, and the password matches you can login!
     * On every login the above mentioned points are checked.
     * For example: If a user is deactived, the next login would fail but the current session would be valid until logout.
     *
     * One word of caution! There are further options which beginning with GROUP_ASSIGNMENT_***. This are also influence the login behavior.
     */
    const SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK = [
            /* ON_LOGIN_CREATE_USER
             * If this is true and the user is not found in database (maybe first login or other reasons),
             * a LDAP query would search for in Active Directory.
             *
             * If the user is found a new UserDbLdap object would be created, the status is refreshed,
             * and the groups are assigned according to the rules defined in GROUP_ASSIGNMENT_**** settings.
             *
             * If the password matches a login is possible. For the user it seems like a normal login.
             */
            'ON_LOGIN_CREATE_USER' => true,

            /*
             * ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS
             * If this is set to true. Evertime a user login is done,
             * the group assignments are refreshed according to the rules defined in GROUP_ASSIGNMENT_**** settings.
             */
            'ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS' => true,

            /*
             * ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS
             * If this is set to true. Evertime a user login is done,
             * the status of the user account in Active Directory is queried and stored in database.
             */
            'ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS' => true,

             /*
             * ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS
             * If this is set to true. Evertime a PAGE REFRESH is done,
             * the status of the user account in Active Directory is queried and stored in database.
             */
            'ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS' => false,
        ];

    /**
     * Constant SYNC_OPTIONS_TEMPLATE_ONLY_BACKEND_TASK
     *
     * On login nothing would be done. If the user doesn't exists or the roles assigned to
     * the user are not allowed to login.
     *
     * You has to configure the backend task, which imports all users from active directory.
     * Please configure the basic's as described here in the folder docs/USAGE_WITH_USER_MODEL.md.
     *
     * Follow this steps to run the backend task:
     * cmd> cd c:\xampp\htdocs\basic
     * cmd> yii ldapcmd/import-all-users
     *
     * This command should import all users, which are matching the configured group assignment options.
     * See GROUP_ASSIGNMENT_** options.
     *
     */
    const SYNC_OPTIONS_TEMPLATE_ONLY_BACKEND_TASK = [
            'ON_LOGIN_CREATE_USER' => false,
            'ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS' => false,
            'ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS' => false,
            'ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS' => false
        ];

     /**
     * Constants starting with GROUP_ASSIGNMENT_****
     * This constant defines all options needed, that are influence the Active Directory group to yii2 role matching
     * of the UserDbLdap class.
     *
     * The first main purpose of this constant is to define, if a login without a assigned role is possible.
     *
     * The second purpose is to define which groups are matched to roles. This enables you to match only certain
     * groups to roles. DON'T forget to create a role with the same name as the group in Active Directory.
     *
     * The third purpose is to define how to deal with roles not matching a LDAP group name (remove them or don't touch them).
     *
      *
      *
     * You can configure your own settings in the config/params.php
     *
     *   With predefined constant
     *
     *   return [
     *       //...
     *       'LDAP-Group-Assignment-Options' => Edvlerblog\Adldap2\model\UserDbLdap::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX_WITH_ROLE,
     *       //...
     *   ];
     *
     *   With complete own settings
     *
     *   return [
     *       //...
     *       'LDAP-Group-Assignment-Options' => [
     *                                           'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => '/(.*)/',
     *                                           'REGEX_GROUP_MATCH_IN_LDAP' => '/^(yii2|app)(.*)/', // start with
     *                                           'ADD_GROUPS_FROM_LDAP_MATCHING_REGEX' => true,
     *                                           'REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP' => true,
     *                                           'REMOVE_ONLY_GROUPS_MATCHING_REGEX' => false,
     *                                           'SEARCH_NESTED_GROUPS' => false
     *                                       ],
     *       //...
     *   ];
     *
     *
     * Some options use regex. Here are some exmaples for common use cases:
     * $regex = '/^(yii2)(.*)/'; // Evaluates to true if the beginning of the groupname is yii2. Example yii2_create_post gives true
     * $regex = '/^(yii2|app)(.*)/'; // Evaluates to true if the beginning of the groupname is yii2 OR app. Example yii2_create_post gives true, app_create_post gives true
     * $regex = '/(.*)(yii2)$/'; //  Evaluates to true if the end of the groupname is yii2. Example create_post_yii2 gives true
     * $regex = '/(.*)(yii2|app)$/'; // Evaluates to true if the end of the groupname is yii2 OR app. Example create_post_yii2 gives true, create_post_app gives true
     * $regex = '/^(yii2_complete_group_name)$/'; // Evaluates to true if the complete groupname matches yii2_complete_group_name
     * $regex = '/^(yii2_complete_group_name|another_complete_group_name)$/'; // Evaluates to true if the complete groupname matches yii2_complete_group_name OR another_complete_group_name
     * $regex = '/(yii2)/'; // Evaluates to true if the groupname contains yii2. Example group_yii2_post gives true
     * $regex = '/(yii2|app)/'; // Evaluates to true if the groupname contains yii2. Example group_yii2_post gives true, Example group_app_post gives true,
     * $regex = '/(.*)/'; // Evaluates to true on every groupname
     */

    /**
     * GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX constant
     * If you don't define your own settings the constant GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX is used as default.
     *
     * In short words the settings does the following:
     *  - Login is possible without any role assinged
     *  - Active Directory groups starting with yii2 and matching (name has to be euqal!) a yii2 role are added to the user
     *  - Groups removed in Active Directory are removed in yii2 to. Groups not matching the REGEX_GROUP_MATCH_IN_LDAP are not touched.
     */
    const GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX = [
            /*
             * LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX
             * If the value of this key is null, a user can login without a role assinged!
             *
             * If a regex is given, a role has to be assinged that matches the regex given for this key to successfully login.
             *
             * For example with the regex
             * 'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => '/(.*)/';
             * a user can only login if a role is assigned. But the name can be anything.
             *
             * With this regex
             * 'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => '/^(yii2)(.*)/';
             * a user can only login if a role is assigned which name is starting with yii2.
             */
            'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => null, // no role necceassarry for login

            /*
             * REGEX_GROUP_MATCH_IN_LDAP
             * If a role exists in yii2 which matches a LDAP group (names has to be the same!), it is assigned to the user.
             * The LDAP groups can be filtered with a regex to match only certain groups from LDAP.
             *
             * In this example only groups starting with yii2 and app would be assigned to the user
             * if a corresponding role (again names has to be the same!) exists in yii2.
             */
            'REGEX_GROUP_MATCH_IN_LDAP' => '/^(yii2|app)(.*)/', // groupname start with yii2 or app

            /*
             * ADD_GROUPS_FROM_LDAP_MATCHING_REGEX
             * Groups from Active Directory would be matched with roles in yii2 and added as described in REGEX_GROUP_MATCH_IN_LDAP.
             *
             * Simple words:
             * If you add a group in Active Directory it would be added in yii too.
             */
            'ADD_GROUPS_FROM_LDAP_MATCHING_REGEX' => true,

            /*
             * REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP
             * If this option is true, all roles NOT matching a LDAP group would be remove from the
             * user.
             *
             * If a role does NOT exists in Active Directory it would be removed.
             * As as result the user only have roles assigned, which are exists as groups in Active Directory, the user is member of and which
             * matches a role in yii2.
             */
            'REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP' => false,

            /*
             * REMOVE_ONLY_GROUPS_MATCHING_REGEX
             * If this option is true, only roles would be removed from the user which matches the regex given
             * under REGEX_GROUP_MATCH_IN_LDAP.
             *
             * This means the user always have the roles which are assingned as groups in Active Directory.
             * If you remove one, it would be removed in yii2 too.
             * But other roles assigned directly in yii2 would not be touched.
             */
            'REMOVE_ONLY_GROUPS_MATCHING_REGEX' => true,

            /*
             * Check for groups, which are not directly assigned to the user,
             * but assigned to another group in which the user is member of.
             * (keywords: nested groups, group tree, group as member of group).
             */
            'SEARCH_NESTED_GROUPS' => false
        ];

    /**
     * GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX_WITH_ROLE constant
     * Same as GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX.
     *
     * The only differnce is, that a login is only allowed with a role assigned.
     */
    const GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX_WITH_ROLE = [
            'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => '/(.*)/', // a role has to be assign, the name could be everything
            'REGEX_GROUP_MATCH_IN_LDAP' => '/^(yii2|app)(.*)/', // start with
            'ADD_GROUPS_FROM_LDAP_MATCHING_REGEX' => true,
            'REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP' => true,
            'REMOVE_ONLY_GROUPS_MATCHING_REGEX' => false,
            'SEARCH_NESTED_GROUPS' => false
        ];

    /**
     * GROUP_ASSIGNMENT_LDAP_MANDANTORY constant
     * All roles that are not found in active directory as group will be removed from the user object.
     *
     * The roles assigned to the user object are always the same as the member of attribute of the active directory user.
     */
    const GROUP_ASSIGNMENT_LDAP_MANDANTORY = [
            'LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX' => '/(.*)/',
            'REGEX_GROUP_MATCH_IN_LDAP' => '/^(yii2|app)(.*)/', // start with
            'ADD_GROUPS_FROM_LDAP_MATCHING_REGEX' => true,
            'REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP' => true,
            'REMOVE_ONLY_GROUPS_MATCHING_REGEX' => false,
            'SEARCH_NESTED_GROUPS' => false
        ];

    /**
     * Constants starting with EXTENSION_OPTIONS_****
     *
     * This array defines the default options for the extension.
     *
     * You can configure your own settings in the config/params.php
     *
     *   With predefined constant
     *
     *   return [
     *       //...
     *       'yii2-adldap-extension-Options' => Edvlerblog\Adldap2\model\UserDbLdap::EXTENSION_OPTIONS_DEBUG,
     *       //...
     *   ];
     *
     *   With complete own settings
     *
     *   return [
     *       //...
     *       'yii2-adldap-extension-Options' => [
     *                                           'ENABLE_YII2_PROFILING' => false
     *                                       ],
     *       //...
     *   ];
     */
    const EXTENSION_OPTIONS_DEFAULT = [
            /*
             * Set the name of the adldap provider used.
             * The name is defined in you web.php or main.php
             */
            'adldap-providername' => '__USE_DEFAULT_PROVIDER__',

            /*
             * Disable/enable profiling (yii2-debugger tab Profiling or Timeline), which is used
             * to analyse time needed for each function in the yii2-adldap-module
             */
            'ENABLE_YII2_PROFILING' => false
        ];

    const EXTENSION_OPTIONS_DEBUG = [
            'adldap-providername' => '__USE_DEFAULT_PROVIDER__',
            'ENABLE_YII2_PROFILING' => true
        ];

    /**
     * Constants for a enabeld/disabled which are saved to database.
     */
    const STATUS_DISABLED = 0;
    const STATUS_ENABLED = 1;

    const YII2_PROFILE_NAME = 'Edvlerblog\Adldap2\model\UserDbLdap::';

    private $ldapUserObject = null;
    private $individualSyncOptions = null;
    private $individualGroupAssignmentOptions = null;

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%user}}';
    }

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            TimestampBehavior::className(),
        ];
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            ['status', 'default', 'value' => static::STATUS_DISABLED],
            ['status', 'in', 'range' => [static::STATUS_ENABLED, static::STATUS_DISABLED]],
        ];
    }

    /**
     * @inheritdoc
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new NotSupportedException('Edvlerblog\Adldap2\model\UserDbLdap::findIdentityByAccessToken($token, $type = null) is not implemented.');
    }

    /**
     * @inheritdoc
     */
    public static function findIdentity($id)
    {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('findIdentity', static::YII2_PROFILE_NAME . 'findIdentity');
        }

        //Database check. If no dataset is found then the only possible return value is null.
        $userObjectDb = static::findOne(['id' => $id]);
        $checkedUserObjectDB = static::checkAllowedToLogin($userObjectDb);

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('findIdentity', static::YII2_PROFILE_NAME . 'findIdentity');
        }
        return $checkedUserObjectDB;
    }

    /**
     * Find a user with a LDAP-attribute and a value.
     *
     * If you don't want to use the unsername (samaccountname) for login and
     * for example want to use LDAP-attribute userPrincipalName for the login, call this function with
     * \Edvlerblog\Adldap2\model\UserDbLdap::findByAttribute('userPrincipalName',$this->username);
     * You can use any LDAP-Attribute availiable.
     *
     * This function does a LDAP-Query to the Active Directory to retrive the
     * samaccountname. If exactly one result is returned, the function
     * static::findByUsername(QUERIED_SAM_ACCOUNT_NAME); is called.
     *
     * @param string $attribute Attribut for
     * @param string $searchValue The value, the attribute
     * @return Edvlerblog\Adldap2\model\UserDbLdap A User instance, if user is valid. Otherwise NULL.
     */
    public static function findByAttribute($attribute, $searchValue) {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('Attribute: ' . $attribute . '; Value: ' . $searchValue, static::YII2_PROFILE_NAME . 'findByAttribute');
        }

        $userObjectsFound = static::getAdldapProvider()->search()->select('samaccountname')->where($attribute, '=', $searchValue)->get();

        $userObjectReturn = null;
        if(count($userObjectsFound) == 1) {
            $userObjectReturn = static::findByUsername($userObjectsFound[0]['samaccountname'][0]);
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('Attribute: ' . $attribute . '; Value: ' . $searchValue, static::YII2_PROFILE_NAME . 'findByAttribute');
        }
        return $userObjectReturn;
    }

    /**
     * Finds user by username (samaccountname)
     *
     * Depending on the synchronisation options additional LDAP querys are done.
     *
     * For a description of the options see the top of this class, where templates (e.g. SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK) are defined.
     * This Templates can be used directly in params or you can define each param by yourself.
     *
     * Example config/params.php
     * return [
     *        ...
     *        'LDAP-User-Sync-Options' => Edvlerblog\Adldap2\model\UserDbLdap::SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK,
     *        ...
     * ];
     *
     * If the user does not exists in database and the option [[ON_LOGIN_CREATE_USER]] is true
     * a LDAP query would be done to find the user in LDAP and sync it to database.
     *
     * If the [[ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS]] option is true, the group assigment is
     * queryied from LDAP and stored in database on login.
     *
     * If the [[ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS]] option is true, the account status is
     * queryied from LDAP and stored in database on login.
     *
     * @param string $username username of the user object
     * @return Edvlerblog\Adldap2\model\UserDbLdap A User instance, if user is valid. Otherwise NULL.
     */
    public static function findByUsername($username) {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('findByUsername', static::YII2_PROFILE_NAME . 'findByUsername');
        }

        $userObjectDb = static::findOne(['username' => $username]);

        //Create user if not found in db
        if ($userObjectDb == null) {
            //Just create to get synchronisation options
            $userObjectDb = new static();

            if(static::getSyncOptions('ON_LOGIN_CREATE_USER', $userObjectDb->individualSyncOptions) == true) {
                $userObjectDb = static::createNewUser($username);
            } else {
                $userObjectDb = null;
            }
        } else {
            //Refresh group assignments of user if found in database
            if (static::getSyncOptions('ON_LOGIN_REFRESH_GROUP_ASSIGNMENTS', $userObjectDb->individualSyncOptions) == true) {
                $userObjectDb->updateGroupAssignment();
            }

            //Refresh account status of user if found in database
            if (static::getSyncOptions('ON_LOGIN_REFRESH_LDAP_ACCOUNT_STATUS', $userObjectDb->individualSyncOptions) == true &&
                static::getSyncOptions('ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS', $userObjectDb->individualSyncOptions) == false)
            {
                $userObjectDb->updateAccountStatus();
            }
        }

        $checkedUserObjectDB = static::checkAllowedToLogin($userObjectDb);
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('findByUsername', static::YII2_PROFILE_NAME . 'findByUsername');
        }
        return $checkedUserObjectDB;
    }

    /**
     * Check if a [[Edvlerblog\Adldap2\model\UserDbLdap]] is allowed to login.
     * Two checks are done before a user object is returned.
     *
     * 1. Check if user is enabled
     * If [[ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS]] option is true, the account status is
     * queryied ON EVERY REQUEST from LDAP and stored in database on login.
     *
     * 2. Check if the user has a role assigned which is allowed to login
     * See Parameter LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX
     *
     * @param Edvlerblog\Adldap2\model\UserDbLdap $userObjectDb User object to validate.
     * @return Edvlerblog\Adldap2\model\UserDbLdap A User instance if user is valid. Otherwise NULL.
     */
    public static function checkAllowedToLogin($userObjectDb) {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('checkAllowedToLogin', static::YII2_PROFILE_NAME . 'checkAllowedToLogin');
        }

        $userInstanceAfterLogin = null;

        if ($userObjectDb != null &&
            $userObjectDb->username != null &&
            $userObjectDb->getId() != null
            ) {
            //Refresh account status on every request?
            if (static::getSyncOptions('ON_REQUEST_REFRESH_LDAP_ACCOUNT_STATUS', $userObjectDb->individualSyncOptions) == true) {
                $userObjectDb->updateAccountStatus();
            }

            //Login only possible if a role is assigned which matches the LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX regex
            if ($userObjectDb->status == static::STATUS_ENABLED &&
                static::getGroupAssigmentOptions('LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX',$userObjectDb->individualGroupAssignmentOptions) != null)
            {
                $rolesAssignedToUser = Yii::$app->authManager->getRolesByUser($userObjectDb->getId());

                foreach ($rolesAssignedToUser as $role) {
                    if(preg_match(static::getGroupAssigmentOptions('LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX',$userObjectDb->individualGroupAssignmentOptions),$role->name) == true) {
                        $userInstanceAfterLogin = $userObjectDb;
                        break;
                    }
                }
            }

            //Login possible if no role is assigned
            if ($userObjectDb->status == static::STATUS_ENABLED &&
                static::getGroupAssigmentOptions('LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX',$userObjectDb->individualGroupAssignmentOptions) == null)
            {
                $userInstanceAfterLogin = $userObjectDb;
            }
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('checkAllowedToLogin', static::YII2_PROFILE_NAME . 'checkAllowedToLogin');
        }
        return $userInstanceAfterLogin;
    }

    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->getPrimaryKey();
    }

    /**
     * @inheritdoc
     */
    public function getAuthKey()
    {
        //Fallback if for some reason no auth key exists
        if ($this->auth_key == null) {
            $this->generateAuthKey();
            $this->save();
        }
        return $this->auth_key;
    }
    /**
     * @inheritdoc
     */
    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }

    /**
     * Validates password
     *
     * @param string $password password to validate
     * @return bool if password provided is valid for current user
     */
    public function validatePassword($password)
    {
        //is yii2 profiling enabled?
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('LDAP validatePassword function', static::YII2_PROFILE_NAME . 'validatePassword');
        }

        $passwordValid = static::getAdldapProvider()->auth()->attempt($this->username,$password);

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('LDAP validatePassword function', static::YII2_PROFILE_NAME . 'validatePassword');
        }
        return $passwordValid;
    }

    /**
     * Generates "remember me" authentication key
     */
    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }

    /**
     * Create a new object in database.
     *
     * @param string $username username of the LDAP user.
     * @return Edvlerblog\Adldap2\model\UserDbLdap object. Null if the username is not found in LDAP.
     */
    public static function createNewUser($username,$individualGroupAssignmentOptions = null) {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('createNewUser', static::YII2_PROFILE_NAME . 'createNewUser');
        }

        $userObjectDb = new static();

        //Username has to be set before a LDAP query
        $userObjectDb->username = $username;
        $userObjectDb->setIndividualGroupAssignmentOptions($individualGroupAssignmentOptions);

        //Check if user exists in LDAP.
        if($userObjectDb->queryLdapUserObject() == null) {
            $userObjectDb = null;
        } else {
            $roles = $userObjectDb->updateGroupAssignment();

            //When a group is needed for login and no roles are assigned to user, don't create one
            if (count($roles) > 0 || static::getGroupAssigmentOptions('LOGIN_POSSIBLE_WITH_ROLE_ASSIGNED_MATCHING_REGEX',$userObjectDb->individualGroupAssignmentOptions) == null) {
                $userObjectDb->generateAuthKey();
                $userObjectDb->updateAccountStatus();
                $userObjectDb->save();
            } else {
                $userObjectDb = null;
            }
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('createNewUser', static::YII2_PROFILE_NAME . 'createNewUser');
        }
        return $userObjectDb;
    }

    /**
     * Check if a user exists. If the user exists the account status and group assigments are refreshed.
     * Otherwise a new user is created.
     *
     * @param string $username
     * @param array $individualGroupAssignmentOptions
     * @return Edvlerblog\Adldap2\model\UserDbLdap object. Null if the username is not found in LDAP.
     */
    public static function createOrRefreshUser($username,$individualGroupAssignmentOptions = null) {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('createOrRefreshUser', static::YII2_PROFILE_NAME . 'createOrRefreshUser');
        }

        $userObjectDb = static::findOne(['username' => $username]);

        //Create user if not found in db
        if ($userObjectDb == null) {
            $userObjectDb = static::createNewUser($username, $individualGroupAssignmentOptions);
        } else {
            $userObjectDb->setIndividualGroupAssignmentOptions($individualGroupAssignmentOptions);
            $userObjectDb->updateAccountStatus();
            $userObjectDb->updateGroupAssignment();
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('createOrRefreshUser', static::YII2_PROFILE_NAME . 'createOrRefreshUser');
        }
        return $userObjectDb;
    }

    /**
     * Query LDAP for the current user status and save the information to database.
     *
     * @return int Status after update
     */
    public function updateAccountStatus() {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('LDAP updateAccountStatus function', static::YII2_PROFILE_NAME . 'updateAccountStatus');
        }

        $ldapUser = $this->queryLdapUserObject();

        if ($ldapUser == null) {
            //If no user is found in LDAP, disable in database.
            $this->status = static::STATUS_DISABLED;
        } else {
            //Query account status from active directory
            $ldapAccountState = $ldapUser->getUserAccountControl();

            $disabledUser = ($ldapAccountState & AccountControl::ACCOUNTDISABLE) === AccountControl::ACCOUNTDISABLE;
            $lockedUser = ($ldapAccountState & AccountControl::LOCKOUT) === AccountControl::LOCKOUT;
            $pwExpired = ($ldapAccountState & AccountControl::PASSWORD_EXPIRED) === AccountControl::PASSWORD_EXPIRED;

            if($disabledUser == true || $lockedUser == true || $pwExpired == true) {
                $this->status = static::STATUS_DISABLED;
            } else {
                $this->status = static::STATUS_ENABLED;
            }
        }

        $this->save();

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('LDAP updateAccountStatus function', static::YII2_PROFILE_NAME . 'updateAccountStatus');
        }

        return $this->status;
    }

    /**
     * Update the group assignment of the user object
     * The Yii::$app->params['LDAP-Group-Assignment-Options'] has several options how to update the group assignment.
     *
     * Basicly a query to LDAP is done which returns the groups assigned to the user in the LDAP directory.
     * Depending on the settings in the params groups are added or removed from the user object.
     *
     * For a description of the options see the top of this class, where templates (e.g. GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX) are defined.
     * This Templates can be used directly in params or you can define each param by yourself.
     *
     * Example config/params.php
     * return [
     *        ...
     *        'LDAP-Group-Assignment-Options' => Edvlerblog\Adldap2\model\UserDbLdap::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX,
     *        ...
     * ];
     *
     * @return Role[] all roles directly assigned to the user. The array is indexed by the role names.
     */
    public function updateGroupAssignment() {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('updateGroupAssignment', static::YII2_PROFILE_NAME . 'updateGroupAssignment');
        }

        $ldapGroupsAssignedToUser = $this->getGroupsAssignedInLdap(); //Query LDAP groups assigned to user
        $yiiRolesAssignedToUser = Yii::$app->authManager->getRolesByUser($this->getId()); //Get all roles assigned to user
        $yiiAvailableRoles = Yii::$app->authManager->getRoles(); //Get all avaliable roles in yii2

        //Map groups from LDAP to roles and add to user object.
        if (static::getGroupAssigmentOptions('ADD_GROUPS_FROM_LDAP_MATCHING_REGEX',$this->individualGroupAssignmentOptions) == true) {
            foreach ($ldapGroupsAssignedToUser as $gn) {
                if(preg_match(static::getGroupAssigmentOptions('REGEX_GROUP_MATCH_IN_LDAP',$this->individualGroupAssignmentOptions),$gn) == true) {
                    if(array_key_exists($gn,$yiiAvailableRoles) && !array_key_exists($gn,$yiiRolesAssignedToUser)) {
                        if ($this->isNewRecord) {
                            $this->generateAuthKey();
                            $this->updateAccountStatus();
                            $this->save(); //Save to db to get id from database
                        }
                        $auth = Yii::$app->authManager;
                        $role = $auth->getRole($gn);
                        $auth->assign($role, $this->getId());
                    }
                }
            }
        }

        //Remove all roles from user object which are not in LDAP
        if (static::getGroupAssigmentOptions('REMOVE_ALL_GROUPS_NOT_FOUND_IN_LDAP',$this->individualGroupAssignmentOptions) == true &&
            static::getGroupAssigmentOptions('REMOVE_ONLY_GROUPS_MATCHING_REGEX',$this->individualGroupAssignmentOptions) == false) {
            foreach ($yiiRolesAssignedToUser as $role) {
                if(in_array($role->name,$ldapGroupsAssignedToUser) == false) {
                        $auth = Yii::$app->authManager;
                        $auth->revoke($role, $this->getId());
                }
            }
        }

        //Remove all roles from user object which are matching the regex and are not in LDAP
        if (static::getGroupAssigmentOptions('REMOVE_ONLY_GROUPS_MATCHING_REGEX',$this->individualGroupAssignmentOptions) == true) {
            foreach ($yiiRolesAssignedToUser as $role) {
                $roleName = $role->name;

                if(preg_match(static::getGroupAssigmentOptions('REGEX_GROUP_MATCH_IN_LDAP',$this->individualGroupAssignmentOptions),$roleName) == true &&
                   in_array($roleName,$ldapGroupsAssignedToUser) == false) {
                            $auth = Yii::$app->authManager;
                            $auth->revoke($role, $this->getId());
                }
            }
        }

        $rolesAfterUpdate = Yii::$app->authManager->getRolesByUser($this->getId());

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('updateGroupAssignment', static::YII2_PROFILE_NAME . 'updateGroupAssignment');
        }

        //Return assigned roles.
        return $rolesAfterUpdate;
    }

    /**
     * Query all groups assigned to user from Active Directory.
     * If the parameter SEARCH_NESTED_GROUPS = true then all nested groups are
     * respected too.
     * Keep in mind, that a query for nested groups is much slower as a normal query.
     *
     * @return array with names of groups assigned to user. Empty if no groups found.
     */
    public function getGroupsAssignedInLdap() {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('getGroupsAssignedInLdap', static::YII2_PROFILE_NAME . 'getGroupsAssignedInLdap');
        }

        $ldapUser = $this->queryLdapUserObject();

        $ldapGroupsConverted = []; //start with empty array of groups

        if ($ldapUser != null) {
            //check for nested groups?
            if (static::getGroupAssigmentOptions('SEARCH_NESTED_GROUPS',$this->individualGroupAssignmentOptions) == true) {
                //$ldapGroups=$ldapUser->getGroups(['cn'], $recursive=true); //alternate Query, but slower
                //1.2.840.113556.1.4.1941 = Specical OID to resolve chains
                $ldapGroups = static::getAdldapProvider()->search()->rawFilter('(member:1.2.840.113556.1.4.1941:=' . $ldapUser->getDn() . ')')->select('cn')->raw()->get();
                if ($ldapGroups == null) {
                    $ldapGroups = [];
                }

                //get cn of each group
                foreach ($ldapGroups as $groupDn) {
                    if (is_array($groupDn) && array_key_exists('cn', $groupDn)) {
                        array_push($ldapGroupsConverted, $groupDn['cn'][0]);
                    }
                }
            } else {
                //get attribute memberof
                $ldapGroups = $ldapUser->getAttribute('memberof');
                if ($ldapGroups == null) {
                    $ldapGroups = [];
                }

                //get first part of dn
                foreach ($ldapGroups as $groupDn) {
                    $n = Utilities::explodeDn($groupDn)[0];
                    array_push($ldapGroupsConverted, $n);
                }
            }
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('getGroupsAssignedInLdap', static::YII2_PROFILE_NAME . 'getGroupsAssignedInLdap');
        }
        return $ldapGroupsConverted;
    }

    /**
     * Querys the complete user object from LDAP.
     * The username of the object has to be set before a query!
     *
     * @return \Adldap\models\User
     * @throws \yii\base\Exception if the username is not set and thus no LDAP query based on username can be done.
     */
    public function queryLdapUserObject() {
        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::beginProfile('queryLdapUserObject', static::YII2_PROFILE_NAME . 'queryLdapUserObject');
        }

        if ($this->ldapUserObject == null) {
            if ($this->username == null) {
                throw new \yii\base\Exception('Please set username attribute before calling queryLdapUserObject() function.');
            }

            $userObjectsFound = static::getAdldapProvider()->search()->where('sAMAccountname', '=', $this->username)->get();

            if(count($userObjectsFound) != 1) {
                $this->ldapUserObject = null;
            } else {
                $this->ldapUserObject = $userObjectsFound[0];
            }
        }

        if(static::getExtensionOptions('ENABLE_YII2_PROFILING') == true) {
            Yii::endProfile('queryLdapUserObject', static::YII2_PROFILE_NAME . 'queryLdapUserObject');
        }

        return $this->ldapUserObject;
    }

    /**
     * Get the Adldap2 provider name
     * Since Version 3.0.5 use the static function getAdldapProvider()
     *
     * @deprecated since version 3.0.5
     */
    private function getAdldap2Provider() {
        if(isset(Yii::$app->params['yii2-adldap-providername'])) {
            $provider =Yii::$app->ad->getProvider(Yii::$app->params['yii2-adldap-providername']);
        } else {
            $provider =Yii::$app->ad->getDefaultProvider();
        }

        return $provider;
    }

    /**
     * Get the Adldap2 provider name from the extension options.
     * See constant variable EXTENSION_OPTIONS_DEFAULT at the top of the class
     */
    public static function getAdldapProvider() {
        if(static::getExtensionOptions('adldap-providername') != '__USE_DEFAULT_PROVIDER__') {
            $provider = Yii::$app->ad->getProvider(static::getExtensionOptions('adldap-providername'));
        } else {
            $provider = Yii::$app->ad->getDefaultProvider();
        }

        return $provider;
    }

    /**
     * Set a individual LDAP synchronisation configuration object for this object.
     *
     * @param type $individualSyncOptions See SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK in the code for possible options.
     */
    public function setIndividualSyncOptions($individualSyncOptions) {
        $this->individualSyncOptions = $individualSyncOptions;
    }

    /**
     * Get a value of the synchronisation options by option key.
     * The options can be defined with the setIndividualSyncOptions
     * function of this class for a single object, or global in the yii2 params.php.
     * If nothing is defined the defaults from the varibale SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK
     * are used.
     *
     * @param string $optionName The option key of the value to retrive.
     * @param string $individualSyncOptions Array with individual sync options for this function call only
     * @return mixed The value of the option key
     * @throws \yii\base\Exception if option key is not found in the given option set.
     */
    public static function getSyncOptions($optionName, $individualSyncOptions = null) {
        //try object specific settings
        if ($individualSyncOptions != null &&
            is_array($individualSyncOptions) &&
            array_key_exists($optionName, $individualSyncOptions))
        {
            return $individualSyncOptions[$optionName];
        }

        //try yii2 params
        else if (isset(Yii::$app->params['LDAP-User-Sync-Options']) &&
            is_array(Yii::$app->params['LDAP-User-Sync-Options']) &&
            array_key_exists($optionName, Yii::$app->params['LDAP-User-Sync-Options']))
        {
            return Yii::$app->params['LDAP-User-Sync-Options'][$optionName];
        }

        //default from distribution
        else if (array_key_exists($optionName, static::SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK)) {
            return static::SYNC_OPTIONS_TEMPLATE_WITHOUT_BACKEND_TASK[$optionName];
        }

        //Exception
        else {
            throw new \yii\base\Exception('Sync-option ' . $optionName . ' not found. Please define settings in the config/params.php of the yii2 framework as described on top of the UserDbLdap.php');
        }
    }

    /**
     * Set a individual LDAP group assignment configuration object for this object.
     *
     * @param type $individualGroupAssignmentOptions See GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX in the code for possible options.
     */
    public function setIndividualGroupAssignmentOptions($individualGroupAssignmentOptions) {
        $this->individualGroupAssignmentOptions = $individualGroupAssignmentOptions;
    }

    /**
     * Get a value of the group assignment options by option key.
     * The options can be defined with the setIndividualGroupAssignmentOptions
     * function of this class for a single object, or global in the yii2 params.php.
     * If nothing is defined the defaults from the varibale GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX
     * are used.
     *
     * See function updateGroupAssignment for further explanation.
     *
     * @param string $optionName The option key of the value to retrive.
     * @param string $individualSyncOptions Array with individual group assignment options for this function call only
     * @return mixed The value of the option key
     * @throws \yii\base\Exception if option key is not found in the given option set.
     */
    public static function getGroupAssigmentOptions($optionName, $individualGroupAssignmentOptions = null) {
        //try object specific settings
        if ($individualGroupAssignmentOptions != null &&
            is_array($individualGroupAssignmentOptions) &&
            array_key_exists($optionName, $individualGroupAssignmentOptions))
        {
            return $individualGroupAssignmentOptions[$optionName];
        }

        //try yii2 params
        else if (isset(Yii::$app->params['LDAP-Group-Assignment-Options']) &&
            is_array(Yii::$app->params['LDAP-Group-Assignment-Options']) &&
            array_key_exists($optionName, Yii::$app->params['LDAP-Group-Assignment-Options']))
        {
            return Yii::$app->params['LDAP-Group-Assignment-Options'][$optionName];
        }

        //default from distribution
        else if (array_key_exists($optionName, static::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX)) {
            return static::GROUP_ASSIGNMENT_TOUCH_ONLY_MATCHING_REGEX[$optionName];
        }

        //Exception
        else {
            throw new \yii\base\Exception('Group-Option ' . $optionName . ' not found. Please define settings in the config/params.php of the yii2 framework as described on top of the UserDbLdap.php');
        }
    }

    /**
     * Get a value of the group assignment options by option key.
     * The options can be defined global in the yii2 params.php.
     * If nothing is defined the defaults from the varibale EXTENSION_OPTIONS_DEFAULT
     * are used.
     *
     * @param string $optionName The option key of the value to retrive.
     * @return mixed The value of the option key
     * @throws \yii\base\Exception if option key is not found in the given option set.
     */
    public static function getExtensionOptions($optionName) {
        //try yii2 params
        if (isset(Yii::$app->params['yii2-adldap-extension-Options']) &&
            is_array(Yii::$app->params['yii2-adldap-extension-Options']) &&
            array_key_exists($optionName, Yii::$app->params['yii2-adldap-extension-Options']))
        {
            return Yii::$app->params['yii2-adldap-extension-Options'][$optionName];
        }

        //default from distribution
        else if (array_key_exists($optionName, static::EXTENSION_OPTIONS_DEFAULT)) {
            return static::EXTENSION_OPTIONS_DEFAULT[$optionName];
        }

        //Exception
        else {
            throw new \yii\base\Exception('Extension-Option ' . $optionName . ' not found. Please define settings in the config/params.php of the yii2 framework as described on top of the UserDbLdap.php');
        }
    }

    /**
     * Get the password expirytime
     * See: https://msdn.microsoft.com/en-us/library/cc223410.aspx
     */
    public function getPasswordExpiryDate() {
        if ($this->username == null) {
            throw new \yii\base\Exception('Please set username attribute before calling getPasswordExpiryDate() function.');
        }

        $result = static::getAdldapProvider()->
                  search()->
                  select(['msDS-UserPasswordExpiryTimeComputed'])->
                  where('samaccountname', '=', $this->username)->
                  get();
        return $result[0]->getFirstAttribute('msds-userpasswordexpirytimecomputed');
    }
}


<?php
/**
 * See LICENSE.md distributed with the software package for license informations.
 *
 * THANKS TO:
 * - Fedek. He wrote a similar plugin for yii 1.
 * - stevebauman. He is maintaining the Adldap2 repository on github.com
 *
 * @category yii2-extension
 * @package yii2-adldap-module
 * @author Matthias Maderer
 * @copyright (c) 2017 Matthias Maderer
 * @link https://github.com/edvler/yii2-adldap-module
 */

namespace Edvlerblog\Adldap2;

use yii\base\Component; //include YII component class
use Adldap\Adldap; //include the Adldap class

class Adldap2Wrapper extends Component
{
    /**
     * The Adldap instance.
     */
    public $adLdapInstance;

    /**
     * Array containig providers config
     */
    public $providers;

    /*
     * The name of the default provider
     */
    public $defaultProvider = "default";

    /**
     * init() called by yii.
     */
    public function init()
    {
        if(!isset($this->adLdapInstance)) {
            $this->adLdapInstance = new Adldap();
        }

        foreach($this->providers as $providerName=>$prodivderSettings) {
            $config = new \Adldap\Connections\Provider($prodivderSettings['config']);
            $this->adLdapInstance->addProvider($config, $providerName);

            if (array_key_exists('schema',$prodivderSettings) && is_object($prodivderSettings['schema'])) {
                $this->adLdapInstance->getProvider($providerName)->setSchema($prodivderSettings['schema']);
            }

            if($prodivderSettings['autoconnect'] == true) {
                $this->adLdapInstance->connect($providerName);
            }
        }

        $providers = $this->adLdapInstance->getProviders();

        if (array_key_exists($this->defaultProvider, $providers)) {
            $this->adLdapInstance->setDefaultProvider($this->defaultProvider);
        } else {
            throw new \yii\base\Exception("The given defaultprovder with the name " . $this->defaultProvider . " could not be found. See https://github.com/edvler/yii2-adldap-module/blob/master/readme.md");
        }
    }

    /**
     * Use magic PHP function __call to route ALL function calls to the Adldap class.
     * Look into the Adldap class for possible functions.
     *
     * @param string $methodName Method name from Adldap class
     * @param array $methodParams Parameters pass to method
     * @return mixed
     */
    public function __call($methodName, $methodParams)
    {
        return call_user_func_array([$this->adLdapInstance, $methodName], $methodParams);
    }
}


<?php
/**
 * See LICENSE.md distributed with the software package for license informations.
 *
 * THANKS TO:
 * - Fedek. He wrote a similar plugin for yii 1.
 * - stevebauman. He is maintaining the Adldap2 repository on github.com
 *
 * @category yii2-extension
 * @package yii2-adldap-module
 * @author Matthias Maderer
 * @copyright (c) 2017 Matthias Maderer
 * @link https://github.com/edvler/yii2-adldap-module
 */

namespace Edvlerblog\Adldap2\commands;

use Yii;
use yii\console\Controller;
use Adldap\Utilities;

class LdapController extends Controller
{
    /**
     * Create a role with the name yii2_example_group and assign the permissions permissionDisplayDetailedAbout and permissionToUseContanctPage
     */
    public function actionCreateExampleRole()
    {
        $auth = Yii::$app->authManager;

        // add "permissionDisplayDetailedAbout" permission
        $displayDetailedAbout = $auth->createPermission('permissionDisplayDetailedAbout');
        $displayDetailedAbout->description = 'Permission to display detailed about informations';
        $auth->add($displayDetailedAbout);

        // add "permissionToUseContanctPage" permission
        $useContactPage = $auth->createPermission('permissionToUseContanctPage');
        $useContactPage->description = 'Permission to use the contanct page';
        $auth->add($useContactPage);

        // add "yii2_example_group" role and give this role the "permissionDisplayDetailedAbout" and "permissionToUseContanctPage" permission
        $yii2ExampleGroup = $auth->createRole('yii2_example_group');
        $auth->add($yii2ExampleGroup);
        $auth->addChild($yii2ExampleGroup, $displayDetailedAbout);
        $auth->addChild($yii2ExampleGroup, $useContactPage);

        // add "permissionToUseContanctPage" permission
        $useHomePage = $auth->createPermission('permissionToSeeHome');
        $useHomePage->description = 'Permission to use the home page';
        $auth->add($useHomePage);

        // add "yii2_see_home_group" role and give this role the "permissionToSeeHome" permission
        $yii2HomeGroup = $auth->createRole('yii2_see_home_group');
        $auth->add($yii2HomeGroup);
        $auth->addChild($yii2HomeGroup, $useHomePage);

        echo "\n\n!!!! TODO !!!!\nTow roles with the name yii2_example_group and yii2_see_home_group were created in yii2.\nPlease create the groups with the same name in Active Directory.\nAssign the user you are using for the login to this groups in Active Directory.\n";
    }

    /**
     * Import all users from LDAP, assign roles and account status. Run this command with cron or another scheduler every X minuten/hours/days.
     */
    public function actionImportAllUsers()
    {
        \Yii::warning("-- Starting import from Active Directory --");
        $results = \Yii::$app->ad->getDefaultProvider()->
                search()->
                select("samaccountname")->
                where('objectClass','=','user')->
                where('objectCategory','=','person')->
                paginate(999);

        $userNamesToAdd = [];
        foreach($results->getResults() as $ldapUser) {
            $accountName = $ldapUser->getAttribute("samaccountname",0);
            if ($accountName != null) {
                array_push($userNamesToAdd, $accountName);
            }
        }

        $c=0;
        \Yii::warning("-- Found " . count ($userNamesToAdd) . " users --");
        foreach ($userNamesToAdd as $userName) {
            $c++;

            \Yii::warning("-- Working on user " . $userName . " --");
            $userObject = \Edvlerblog\Adldap2\model\UserDbLdap::createOrRefreshUser($userName);

            if ($userObject != null) {
                \Yii::warning("User " . $userName . " created");
            } else {
                \Yii::warning("User " . $userName . " NOT created");
            }

        }
        \Yii::warning("-- End import from Active Directory --");

     }

}


<?php namespace Acoustep\EntrustGui\Console\Commands;

use Illuminate\Console\Command;

class GenerateModels extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'entrust-gui:models {model=All} {--path=} {--force}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Generate Models';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $path = (($this->option('path')) ? $this->option('path') : app_path()) . '/';
        $model = $this->argument('model');
        $files = $this->getFiles($model);
        foreach ($files as $file) {
            $file_path = $path.$file;
            if ($this->writeable($file_path)) {
                $this->write($file, $file_path);
                $this->info($file.' saved to '.$file_path);
            }
        }
    }

    /**
     * return the files specified
     *
     * @param string
     *
     * @return array
     */
    protected function getFiles($model = 'All')
    {
        $files = [
            'Permission' => 'Permission.php',
            'Role' => 'Role.php',
            'User' => 'User.php',
        ];
        if ($model === 'All') {
            return $files;
        } else {
            return [$model => $files[$model]];
        }

    }
    /**
     * return if the file path is writable
     *
     * @param string
     *
     * @return boolean
     */
    protected function writeable($file_path)
    {
        if ($this->option('force')) {
            return true;
        }
        return  ( ! file_exists($file_path) || $this->confirmable($file_path));
    }

    protected function confirmable($file_path)
    {
        return (file_exists($file_path) && $this->confirm('Overwrite '.$file_path.'? [y|N]'));
    }

    /**
     * write the file to the file path
     *
     * @param string
     * @param string
     *
     * @return void
     */
    protected function write($file, $file_path)
    {
        if (! is_dir(dirname($file_path))) {
            mkdir(dirname($file_path), 0755, true);
        }
        file_put_contents(
            $file_path,
            file_get_contents(
                dirname(__FILE__)
                .DIRECTORY_SEPARATOR
                .'..'.DIRECTORY_SEPARATOR
                .'..'.DIRECTORY_SEPARATOR
                .'..'.DIRECTORY_SEPARATOR
                .'templates'
                .DIRECTORY_SEPARATOR
                .$file
                .'.txt'
            )
        );
    }
}


<?php

namespace Acoustep\EntrustGui\Repositories;

use Prettus\Repository\Eloquent\BaseRepository;
use Prettus\Repository\Criteria\RequestCriteria;
use Config;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
abstract class ManyToManyRepositoryEloquent extends BaseRepository implements RoleRepository
{

    /**
     * Specify Model class name
     *
     * @return string
     */
    public function model()
    {
        return Config::get('entrust.'.$this->getModelName());
    }

    /**
     * Boot up the repository, pushing criteria
     */
    public function boot()
    {
        $this->pushCriteria(app(RequestCriteria::class));
    }

    /**
     * Update attributes
     *
     * @param array $attributes
     * @param integer $id
     *
     * @return Model
     */
    public function update(array $attributes, $id)
    {
        $defaults = ["{$this->getRelationName()}" => []];
        $attributes = array_merge($defaults, $attributes);
        $model = parent::update($attributes, $id);
        $model->{$this->getShortRelationName()}()->sync($attributes["{$this->getRelationName()}"]);
        return $this->parserResult($model);
    }
}


<?php namespace Acoustep\EntrustGui\Http\Controllers;

use Illuminate\Routing\Controller as Controller;
use Acoustep\EntrustGui\Gateways\UserGateway;
use Illuminate\Http\Request;
use Watson\Validating\ValidationException;
use Illuminate\Config\Repository as Config;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
class UsersController extends Controller
{

    protected $gateway;
    protected $request;
    protected $role;
    protected $config;

    /**
     * Create a new UsersController instance.
     *
     * @param Request $request
     * @param UserGateway $gateway
     * @param Config $config
     *
     * @return void
     */
    public function __construct(Request $request, UserGateway $gateway, Config $config)
    {
        $this->config = $config;
        $this->request = $request;
        $this->gateway = $gateway;
        $role_class = $this->config->get('entrust.role');
        $this->role = new $role_class;
    }

    /**
     * Display a listing of the resource.
     * GET /roles
     *
     * @return Response
     */
    public function index()
    {
        $users = $this->gateway->paginate($this->config->get('entrust-gui.pagination.users'));

        return view(
            'entrust-gui::users.index',
            compact(
                'users'
            )
        );
    }

    /**
     * Show the form for creating a new resource.
     * GET /roles/create
     *
     * @return Response
     */
    public function create()
    {
        $user_class = $this->config->get('auth.model');
        $user = new $user_class;
        $roles = $this->role->lists('name', 'id');

        return view(
            'entrust-gui::users.create',
            compact(
                'user',
                'roles'
            )
        );
    }

    /**
     * Store a newly created resource in storage.
     * POST /roles
     *
     * @return Response
     */
    public function store()
    {
        try {
            $user = $this->gateway->create($this->request);
        } catch (ValidationException $e) {
            return redirect(route('entrust-gui::users.create'))
                ->withErrors($e->getErrors())
                ->withInput();
        }
        return redirect(route('entrust-gui::users.index'))
            ->withSuccess(trans('entrust-gui::users.created'));

    }

    /**
     * Show the form for editing the specified resource.
     * GET /roles/{id}/edit
     *
     * @param int $id
     *
     * @return Response
     */
    public function edit($id)
    {
        $user = $this->gateway->find($id);
        $roles = $this->role->lists('name', 'id');

        return view(
            'entrust-gui::users.edit',
            compact(
                'user',
                'roles'
            )
        );

    }

    /**
     * Update the specified resource in storage.
     * PUT /roles/{id}
     *
     * @param int $id
     *
     * @return Response
     */
    public function update($id)
    {
        try {
            $this->gateway->update($this->request, $id);
        } catch (ValidationException $e) {
            return back()->withErrors($e->getErrors())->withInput();
        }
        return redirect(route('entrust-gui::users.index'))
            ->withSuccess(trans('entrust-gui::users.updated'));
    }

    /**
     * Remove the specified resource from storage.
     * DELETE /roles/{id}
     *
     * @param int $id
     *
     * @return Response
     */
    public function destroy($id)
    {
        if (!config('entrust-gui.users.deletable')) {
            abort(404);
        }
        $this->gateway->delete($id);
        return redirect(route('entrust-gui::users.index'))
            ->withSuccess(trans('entrust-gui::users.destroyed'));
    }
}


<?php namespace Acoustep\EntrustGui\Gateways;

use Acoustep\EntrustGui\Repositories\UserRepository;
use Acoustep\EntrustGui\Events\UserCreatedEvent;
use Acoustep\EntrustGui\Events\UserUpdatedEvent;
use Acoustep\EntrustGui\Events\UserDeletedEvent;
use Acoustep\EntrustGui\Traits\DeleteModelTrait;
use Acoustep\EntrustGui\Traits\FindModelTrait;
use Acoustep\EntrustGui\Traits\GetPermissionUserRelationNameTrait;
use Acoustep\EntrustGui\Traits\PaginationGatewayTrait;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Config\Repository as Config;
use Illuminate\Events\Dispatcher;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
class UserGateway implements ManyToManyGatewayInterface
{

    use PaginationGatewayTrait, FindModelTrait, DeleteModelTrait, GetPermissionUserRelationNameTrait;

    protected $repository;
    protected $role;
    protected $config;
    protected $dispatcher;
    protected $hash;

    /**
     * Create a new gateway instance.
     *
     * @param Config $config
     * @param UserRepository $permission_repository
     * @param Dispatcher $dispatcher
     * @param Hasher $hash
     *
     * @return void
     */
    public function __construct(Config $config, UserRepository $repository, Dispatcher $dispatcher, Hasher $hash)
    {
        $this->config = $config;
        $this->repository = $repository;
        $role_class = $this->config->get('entrust.role');
        $this->role = new $role_class;
        $this->dispatcher = $dispatcher;
        $this->hash = $hash;
    }

    /**
     * Create a user
     *
     * @param Illuminate\Http\Request $request
     *
     * @return Illuminate\Database\Eloquent\Model
     */
    public function create($request)
    {
        $data = $request->all();
        $user = $this->repository->create($data);

        $event_class = "Acoustep\EntrustGui\Events\\".ucwords($this->getModelName()).'CreatedEvent';
        $event = new $event_class;
        $this->dispatcher->fire($event->setModel($user));
        return $user;
    }

    /**
     * Update user
     *
     * @param Illuminate\Http\Request $request
     * @param integer $id
     *
     * @return Illuminate\Database\Eloquent\Model
     */
    public function update($request, $id)
    {
        $data = $request->except('password', 'password_confirmation');
        if ($request->has('password')) {
            $data['password'] = $request->get('password');
            $data['password_confirmation'] = $request->get('password_confirmation');
        }
        $user = $this->repository->update($data, $id);
        $event_class = "Acoustep\EntrustGui\Events\\".ucwords($this->getModelName()).'UpdatedEvent';
        $event = new $event_class;
        $this->dispatcher->fire($event->setModel($user));
        return $user;
    }

    /**
     * Return model name
     *
     *
     * @return string
     */
    public function getModelName()
    {
        return 'user';
    }
}


<?php namespace Acoustep\EntrustGui\Traits;

trait DeleteModelTrait
{

    /**
     * Delete model
     *
     * @param integer $id
     *
     * @return void
     */
    public function delete($id)
    {
        $model = $this->repository->find($id);
        $this->repository->delete($id);
        $event_class = "Acoustep\EntrustGui\Events\\".ucwords($this->getModelName()).'DeletedEvent';
        $event = new $event_class;
        $this->dispatcher->fire($event->setModel($model));
    }
}


<?php namespace Acoustep\EntrustGui;

use Acoustep\EntrustGui\Console\Commands\GenerateModels;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Config;

/**
 * EntrustGuiServiceProvider
 *
 * @author Mitch Stanley <acoustep+entrustgui@fastmail.co.uk>
 */
class EntrustGuiServiceProvider extends ServiceProvider
{

    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Boot and configure the application paths
     *
     * @return void
     */
    public function boot()
    {

        $this->loadViewsFrom(realpath(__DIR__.'/../views'), 'entrust-gui');
        $this->setupRoutes($this->app->router);
        $this->loadTranslationsFrom(realpath(__DIR__.'/../translations'), 'entrust-gui');

        // This is for conig
        $this->publishes(
            [__DIR__.'/../config/entrust-gui.php' => config_path('entrust-gui.php')],
            'config'
        );

        // This is for views
        $this->publishes(
            [__DIR__.'/../views' => base_path('resources/views/vendor/entrust-gui')],
            'views'
        );

        // This is for translations
        $this->publishes(
            [__DIR__.'/../translations' => base_path('resources/lang/vendor/entrust-gui')],
            'translations'
        );
        // Register commands
        $this->commands('command.entrust-gui.models');
    }

    /**
     * Define the routes for the application.
     *
     * @param \Illuminate\Routing\Router $router
     *
     * @return void
     */
    public function setupRoutes(Router $router)
    {
        $router->group(
            ['namespace' => 'Acoustep\EntrustGui\Http\Controllers'],
            function ($router) {
                  include __DIR__.'/Http/routes.php';
            }
        );
    }

    /**
    * Register the service provider.
    *
    * @return void
    */
    public function register()
    {
        $this->app->register('Zizaco\Entrust\EntrustServiceProvider');
        $this->app->register('Prettus\Repository\Providers\RepositoryServiceProvider');
        $this->app->bind(
            'Acoustep\EntrustGui\Repositories\UserRepository',
            'Acoustep\EntrustGui\Repositories\UserRepositoryEloquent'
        );
        $this->app->bind(
            'Acoustep\EntrustGui\Repositories\RoleRepository',
            'Acoustep\EntrustGui\Repositories\RoleRepositoryEloquent'
        );
        $this->app->bind(
            'Acoustep\EntrustGui\Repositories\PermissionRepository',
            'Acoustep\EntrustGui\Repositories\PermissionRepositoryEloquent'
        );

        $this->registerCommands();
    }
    /**
    * Register commands.
    *
    * @return void
    */
    protected function registerCommands()
    {
        $this->registerGenerateModelsCommand();
    }

    /**
    * Register the export models command.
    *
    * @return void
    */
    protected function registerGenerateModelsCommand()
    {
        $this->app->singleton('command.entrust-gui.models', function ($app) {
            return new GenerateModels();
        });
    }

    /**
    * List of commands provided by the package.
    *
    * @return void
    */
    public function provides()
    {
        return ['command.entrust-gui.models'];
    }
}


<?php namespace Acoustep\EntrustGui\Http\Controllers;

use Illuminate\Routing\Controller as Controller;
use Acoustep\EntrustGui\Gateways\RoleGateway;
use Illuminate\Http\Request;
use Watson\Validating\ValidationException;
use Illuminate\Config\Repository as Config;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
abstract class ManyToManyController extends Controller
{

    protected $request;
    protected $gateway;
    protected $relation;
    protected $config;
    protected $resource;
    protected $relation_name;

    /**
     * Create a new ManyToManyController instance.
     *
     * @param Request $request
     * @param ManyToManyGateway $gateway
     * @param Config $config
     *
     * @return void
     */
    public function __construct(Request $request, Config $config, $gateway, $resource, $relation)
    {
        $this->config = $config;
        $this->request = $request;
        $this->gateway = $gateway;
        $relation_class = $this->config->get('entrust.'.$relation);
        $this->relation = new $relation_class;
        $this->resource = $resource;
    }

    /**
     * Display a listing of the resource.
     * GET /model
     *
     * @return Response
     */
    public function index()
    {
        $models = $this->gateway->paginate($this->config->get('entrust-gui.pagination.'.$this->resource));

        return view('entrust-gui::'.$this->resource.'.index', compact(
            "models"
        ));
    }

    /**
     * Show the form for creating a new resource.
     * GET /model/create
     *
     * @return Response
     */
    public function create()
    {
        $model_class = $this->config->get('entrust.'.str_singular($this->resource));
        $model = new $model_class;
        $relations = $this->relation->lists('name', 'id');

        return view('entrust-gui::'.$this->resource.'.create', compact(
            'model',
            'relations'
        ));
    }

    /**
     * Store a newly created resource in storage.
     * POST /model
     *
     * @return Response
     */
    public function store()
    {
        try {
            $this->gateway->create($this->request);
        } catch (ValidationException $e) {
            return back()->withErrors($e->getErrors())->withInput();
        }
        return redirect(
            route(
                'entrust-gui::'.$this->resource.'.index'
            )
        )->withSuccess(
            trans(
                'entrust-gui::'.$this->resource.'.created'
            )
        );
    }

    /**
     * Show the form for editing the specified resource.
     * GET /model/{id}/edit
     *
     * @param int $id
     *
     * @return Response
     */
    public function edit($id)
    {
        $model = $this->gateway->find($id);
        $relations = $this->relation->lists('name', 'id');

        return view('entrust-gui::'.$this->resource.'.edit', compact(
            'model',
            'relations'
        ));
    }

    /**
     * Update the specified resource in storage.
     * PUT /model/{id}
     *
     * @param int $id
     *
     * @return Response
     */
    public function update($id)
    {
        try {
            $role = $this->gateway->update($this->request, $id);
        } catch (ValidationException $e) {
            return back()->withErrors($e->getErrors())->withInput();
        }
        return redirect(
            route(
                'entrust-gui::'.$this->resource.'.index'
            )
        )->withSuccess(
            trans(
                'entrust-gui::'.$this->resource.'.updated'
            )
        );
    }

    /**
     * Remove the specified resource from storage.
     * DELETE /model/{id}
     *
     * @param int $id
     *
     * @return Response
     */
    public function destroy($id)
    {
        $this->gateway->delete($id);
        return redirect(
            route(
                'entrust-gui::'.$this->resource.'.index'
            )
        )->withSuccess(
            trans(
                'entrust-gui::'.$this->resource.'.destroyed'
            )
        );
    }
}


<?php namespace Acoustep\EntrustGui\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Config\Repository as Config;
use Illuminate\Http\Response;
use Illuminate\Routing\Redirector;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * From http://stackoverflow.com/a/29186175
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
class AdminAuth
{

    protected $auth;
    protected $config;
    protected $response;
    protected $redirect;

    /**
     * Create a new AdminAuth instance.
     *
     * @param Guard $auth
     *
     * @return void
     */
    public function __construct(Guard $auth, Config $config, Response $response, Redirector $redirect)
    {
        $this->auth = $auth;
        $this->config = $config;
        $this->response = $response;
        $this->redirect = $redirect;
    }

    /**
     * Handle the request
     *
     * @param mixed $request
     * @param Closure $next
     *
     * @return Response
     */
    public function handle($request, Closure $next)
    {
        if ($this->auth->guest()) {
            if ($request->ajax()) {
                return $this->response->make('Unauthorized.', 401);
            } else {
                return $this->redirect->guest($this->config->get('entrust-gui.unauthorized-url', 'auth/login'));
            }
        } elseif (! $request->user()->hasRole($this->config->get('entrust-gui.middleware-role'))) {
            return $this->response->make('Unauthorized.', 401); //Or redirect() or whatever you want
        }
        return $next($request);
    }
}


<?php namespace Acoustep\EntrustGui\Repositories;

use Prettus\Repository\Eloquent\BaseRepository;
use Prettus\Repository\Criteria\RequestCriteria;
use App\Entities\User;
use Config;
use Log;
use Exception;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
class UserRepositoryEloquent extends BaseRepository implements UserRepository
{
    /**
     * Specify Model class name
     *
     * @return string
     */
    public function model()
    {
        return Config::get('auth.model');
    }

    /**
     * Boot up the repository, pushing criteria
     */
    public function boot()
    {
        $this->pushCriteria(app(RequestCriteria::class));
    }

    /**
     * Create model
     *
     * @param array $attributes
     *
     * @return Model
     */
    public function create(array $attributes)
    {
        $defaults = ['roles' => []];
        $attributes = array_merge($defaults, $attributes);
        $model = parent::create($attributes);
        if (! in_array('Esensi\Model\Contracts\HashingModelInterface', class_implements($model))) {
            throw new Exception(
                "User model must implement Esensi\Model\Contracts\HashingModelInterface.
                Revert to 0.3.* or see upgrade guide for details."
            );
        }
        $model->roles()->sync($attributes['roles']);
        return $this->parserResult($model);
    }

    /**
     * Update attributes
     *
     * @param array $attributes
     * @param integer $id
     *
     * @return Model
     */
    public function update(array $attributes, $id)
    {
        $defaults = ['roles' => []];
        $attributes = array_merge($defaults, $attributes);
        $model = $this->find($id);
        if (! in_array('Esensi\Model\Contracts\HashingModelInterface', class_implements($model))) {
            throw new Exception(
                "User model must implement Esensi\Model\Contracts\HashingModelInterface.
                Revert to 0.3.* or see upgrade guide for details."
            );
        }
        if (! array_key_exists('password', $attributes)) {
            $model->fill($attributes);
            if (Config::get('entrust-gui.confirmable') === true) {
                $model->password_confirmation = $model->password;
            }
            $model->saveWithoutHashing();
        } else {
            $model = parent::update($attributes, $id);
        }
        $model->roles()->sync($attributes['roles']);
        return $this->parserResult($model);
    }
}


<?php namespace Acoustep\EntrustGui\Gateways;

use Prettus\Repository\Eloquent\BaseRepository;
use Acoustep\EntrustGui\Traits\PaginationGatewayTrait;
use Acoustep\EntrustGui\Traits\DeleteModelTrait;
use Acoustep\EntrustGui\Traits\FindModelTrait;
use Illuminate\Config\Repository as Config;
use Illuminate\Events\Dispatcher;

/**
 * This file is part of Entrust GUI,
 * A Laravel 5 GUI for Entrust.
 *
 * @license MIT
 * @package Acoustep\EntrustGui
 */
abstract class ManyToManyGateway
{

    use PaginationGatewayTrait, FindModelTrait, DeleteModelTrait;

    protected $repository;
    protected $config;
    protected $dispatcher;

    /**
     * Create a new gateway instance.
     *
     * @param Config $config
     * @param Repository $repository
     * @param Dispatcher $dispatcher
     *
     * @return void
     */
    public function __construct(Config $config, BaseRepository $repository, Dispatcher $dispatcher)
    {
        $this->config = $config;
        $this->repository = $repository;
        $this->dispatcher = $dispatcher;
    }

    /**
     * Create a model
     *
     * @param Illuminate\Http\Request $request
     *
     * @return Illuminate\Database\Eloquent\Model
     */
    public function create($request)
    {
        $model = $this->repository->create($request->all());
        $model->{$this->getShortRelationName()}()->sync($request->get($this->getRelationName(), []));
        $event_class = "Acoustep\EntrustGui\Events\\".ucwords($this->getModelName()).'CreatedEvent';
        $event = new $event_class;
        $this->dispatcher->fire($event->setModel($model));
        return $model;
    }

    /**
     * Find model by ID
     *
     * @param integer $id
     *
     * @return Illuminate\Database\Eloquent\Model
     */
    public function find($id)
    {
        return $this->repository->with($this->getShortRelationName())->find($id);
    }

    /**
     * Update model
     *
     * @param Illuminate\Http\Request $request
     * @param integer $id
     *
     * @return Illuminate\Database\Eloquent\Model
     */
    public function update($request, $id)
    {
        $model = $this->repository->update($request->all(), $id);
        $event_class = "Acoustep\EntrustGui\Events\\".ucwords($this->getModelName()).'UpdatedEvent';
        $event = new $event_class;
        $this->dispatcher->fire($event->setModel($model));
        return $model;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Alt Three Emoji.
 *
 * (c) Alt Three Services Limited
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace AltThree\Emoji;

use AltThree\Emoji\Repositories\CachingRepository;
use AltThree\Emoji\Repositories\GitHubRepository;
use AltThree\Emoji\Repositories\RepositoryInterface;
use GrahamCampbell\GuzzleFactory\GuzzleFactory;
use Illuminate\Contracts\Container\Container;
use Illuminate\Foundation\Application as LaravelApplication;
use Illuminate\Support\ServiceProvider;
use Laravel\Lumen\Application as LumenApplication;

/**
 * This is the emoji service provider class.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class EmojiServiceProvider extends ServiceProvider
{
    /**
     * Boot the service provider.
     *
     * @return void
     */
    public function boot()
    {
        $this->setupConfig();
    }

    /**
     * Setup the config.
     *
     * @return void
     */
    protected function setupConfig()
    {
        $source = realpath($raw = __DIR__.'/../config/emoji.php') ?: $raw;

        if ($this->app instanceof LaravelApplication && $this->app->runningInConsole()) {
            $this->publishes([$source => config_path('emoji.php')]);
        } elseif ($this->app instanceof LumenApplication) {
            $this->app->configure('emoji');
        }

        $this->mergeConfigFrom($source, 'emoji');
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerRepository();
        $this->registerParser();
    }

    /**
     * Register the repository class.
     *
     * @return void
     */
    protected function registerRepository()
    {
        $this->app->singleton(CachingRepository::class, function (Container $app) {
            $repo = new GitHubRepository(
                GuzzleFactory::make(),
                $app->config->get('emoji.token')
            );

            $cache = $app->cache->store($app->config->get('emoji.connection'));
            $key = $app->config->get('emoji.key', 'emoji');
            $life = (int) $app->config->get('emoji.life', 10080);

            return new CachingRepository($repo, $cache, $key, $life);
        });

        $this->app->alias(CachingRepository::class, RepositoryInterface::class);
    }

    /**
     * Register the parser class.
     *
     * @return void
     */
    protected function registerParser()
    {
        $this->app->singleton(EmojiParser::class, function (Container $app) {
            return new EmojiParser($app->make(RepositoryInterface::class));
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return string[]
     */
    public function provides()
    {
        return [
            //
        ];
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Alt Three Emoji.
 *
 * (c) Alt Three Services Limited
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace AltThree\Emoji;

use AltThree\Emoji\Repositories\RepositoryInterface;
use League\CommonMark\Inline\Element\Image;
use League\CommonMark\Inline\Parser\AbstractInlineParser;
use League\CommonMark\InlineParserContext;

/**
 * This is the emoji parser class.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class EmojiParser extends AbstractInlineParser
{
    /**
     * The emoji repo.
     *
     * @var \AltThree\Emoji\Repositories\RepositoryInterface
     */
    protected $repo;

    /**
     * The emoji mappings.
     *
     * @var string[]|null
     */
    protected $map;

    /**
     * Create a new emoji parser instance.
     *
     * @param \AltThree\Emoji\Repositories\RepositoryInterface $repo
     *
     * @return void
     */
    public function __construct(RepositoryInterface $repo)
    {
        $this->repo = $repo;
    }

    /**
     * Get the characters that must be matched.
     *
     * @return string[]
     */
    public function getCharacters()
    {
        return [':'];
    }

    /**
     * Parse a line and determine if it contains an emoji.
     *
     * If it does, then we do the necessary.
     *
     * @param \League\CommonMark\InlineParserContext $inlineContext
     *
     * @throws \AltThree\Emoji\Exceptions\FetchException
     *
     * @return bool
     */
    public function parse(InlineParserContext $inlineContext)
    {
        $cursor = $inlineContext->getCursor();

        $previous = $cursor->peek(-1);
        if ($previous !== null && $previous !== ' ') {
            return false;
        }

        $saved = $cursor->saveState();

        $cursor->advance();

        $handle = $cursor->match('/^[a-z0-9\+\-_]+:/');

        if (!$handle) {
            $cursor->restoreState($saved);

            return false;
        }

        $next = $cursor->peek(0);

        if ($next !== null && $next !== ' ') {
            $cursor->restoreState($saved);

            return false;
        }

        $key = substr($handle, 0, -1);

        if ($this->map === null) {
            $this->map = $this->repo->get();
        }

        if (!array_key_exists($key, $this->map)) {
            $cursor->restoreState($saved);

            return false;
        }

        $inline = new Image($this->map[$key], $key);
        $inline->data['attributes'] = ['class' => 'emoji', 'data-emoji' => $key];
        $inlineContext->getContainer()->appendChild($inline);

        return true;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Alt Three Emoji.
 *
 * (c) Alt Three Services Limited
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace AltThree\Emoji\Repositories;

use AltThree\Emoji\Exceptions\FetchException;
use Exception;
use GuzzleHttp\ClientInterface;

/**
 * This is the emoji github repository class.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class GitHubRepository implements RepositoryInterface
{
    /**
     * The accept header.
     *
     * @var string
     */
    const ACCEPT_HEADER = 'application/vnd.github.v3+json';

    /**
     * The emoji api url.
     *
     * @var string
     */
    const API_URL = 'https://api.github.com/emojis';

    /**
     * The guzzle client instance.
     *
     * @var \GuzzleHttp\ClientInterface
     */
    protected $client;

    /**
     * The optional api token.
     *
     * @var string|null
     */
    protected $token;

    /**
     * Create a new emoji github repository instance.
     *
     * @param \GuzzleHttp\ClientInterface $client
     * @param string|null                 $token
     *
     * @return void
     */
    public function __construct(ClientInterface $client, string $token = null)
    {
        $this->client = $client;
        $this->token = $token;
    }

    /**
     * Fetch the emoji map.
     *
     * @throws \AltThree\Emoji\Exceptions\FetchException
     *
     * @return array
     */
    public function get()
    {
        try {
            $headers = ['Accept' => self::ACCEPT_HEADER];

            if ($this->token) {
                $headers['Authorization'] = "token {$this->token}";
            }

            $response = $this->client->request('get', self::API_URL, ['headers' => $headers]);

            return (array) json_decode((string) $response->getBody(), true);
        } catch (Exception $e) {
            throw new FetchException($e);
        }
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Alt Three Emoji.
 *
 * (c) Alt Three Services Limited
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace AltThree\Emoji\Repositories;

use AltThree\Emoji\Exceptions\FetchException;
use Exception;
use Illuminate\Contracts\Cache\Repository;

/**
 * This is the emoji caching repository class.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class CachingRepository implements RepositoryInterface
{
    /**
     * The inner repo.
     *
     * @var \AltThree\Emoji\Repositories\RepositoryInterface
     */
    protected $repo;

    /**
     * The cache instance.
     *
     * @var \Illuminate\Contracts\Cache\Repository
     */
    protected $cache;

    /**
     * The cache key.
     *
     * @var string
     */
    protected $key;

    /**
     * The cache life.
     *
     * @var int
     */
    protected $life;

    /**
     * Create a new emoji caching repository instance.
     *
     * @param \AltThree\Emoji\Repositories\RepositoryInterface $repo
     * @param \Illuminate\Contracts\Cache\Repository           $cache
     * @param string                                           $key
     * @param int                                              $life
     *
     * @return void
     */
    public function __construct(RepositoryInterface $repo, Repository $cache, string $key, int $life)
    {
        $this->repo = $repo;
        $this->cache = $cache;
        $this->key = $key;
        $this->life = $life;
    }

    /**
     * Fetch the emoji map.
     *
     * @throws \AltThree\Emoji\Exceptions\FetchException
     *
     * @return array
     */
    public function get()
    {
        try {
            return $this->cache->remember($this->key, $this->life, function () {
                return $this->repo->get();
            });
        } catch (FetchException $e) {
            throw $e;
        } catch (Exception $e) {
            throw new FetchException($e);
        }
    }
}


<?php

namespace Sofa\Eloquence\Metable;

use InvalidArgumentException;
use Illuminate\Database\Eloquent\Model;
use Sofa\Eloquence\Contracts\Attribute as AttributeContract;
use Sofa\Eloquence\Mutator\Mutator;

/**
 * @property array $attributes
 */
class Attribute extends Model implements AttributeContract
{
    /**
     * Attribute mutator instance.
     *
     * @var \Sofa\Eloquence\Contracts\Mutator
     */
    protected static $attributeMutator;

    /**
     * Custom table name.
     *
     * @var string
     */
    protected static $customTable;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'meta_attributes';

    /**
     * Indicates if the model should be timestamped.
     *
     * @var boolean
     */
    public $timestamps = false;

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'meta_id';

    /**
     * @var array
     */
    protected $getterMutators = [
        'array'      => 'json_decode',
        'StdClass'   => 'json_decode',
        'DateTime'   => 'asDateTime',
        Model::class => 'unserialize',
    ];

    /**
     * @var array
     */
    protected $setterMutators = [
        'array'      => 'json_encode',
        'StdClass'   => 'json_encode',
        'DateTime'   => 'fromDateTime',
        Model::class => 'serialize',
    ];

    /**
     * The attributes included in the model's JSON and array form.
     *
     * @var array
     */
    protected $visible = ['meta_key', 'meta_value', 'meta_type', 'meta_group'];

    /**
     * Create new attribute instance.
     *
     * @param string|array  $key
     * @param mixed  $value
     */
    public function __construct($key = null, $value = null, $group = null)
    {
        // default behaviour
        if (is_array($key)) {
            parent::__construct($key);
        } else {
            parent::__construct();

            if (is_string($key)) {
                $this->set($key, $value, $group);
            }
        }
    }

    /**
     * Boot this model.
     *
     * @codeCoverageIgnore
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        if (!isset(static::$attributeMutator)) {
            if (function_exists('app') && app()->bound('eloquence.mutator')) {
                static::$attributeMutator = app('eloquence.mutator');
            } else {
                static::$attributeMutator = new Mutator;
            }
        }
    }

    /**
     * Set the meta attribute.
     *
     * @param string $key
     * @param mixed  $value
     * @param string $group
     */
    protected function set($key, $value, $group = 'default')
    {
        $this->setMetaKey($key);
        $this->setValue($value);
        $this->setMetaGroup($group);
    }

    /**
     * Create new AttributeBag.
     *
     * @param  array  $models
     * @return \Sofa\Eloquence\Metable\AttributeBag
     */
    public function newBag(array $models = [])
    {
        return new AttributeBag($models);
    }

    /**
     * Get the meta attribute value.
     *
     * @return mixed
     */
    public function getValue()
    {
        if ($this->hasMutator($this->attributes['meta_value'], 'getter', $this->attributes['meta_type'])) {
            return $this->mutateValue($this->attributes['meta_value'], 'getter');
        }

        return $this->castValue();
    }

    /**
     * Get the meta attribute key.
     *
     * @return string
     */
    public function getMetaKey()
    {
        return $this->attributes['meta_key'];
    }

    /**
     * Get the meta attribute group.
     *
     * @return string
     */
    public function getMetaGroup()
    {
        return $this->attributes['meta_group'];
    }
    /**
     * Cast value to proper type.
     *
     * @return mixed
     */
    protected function castValue()
    {
        $value = $this->attributes['meta_value'];

        $validTypes = ['boolean', 'integer', 'float', 'double', 'array', 'object', 'null'];

        if (in_array($this->attributes['meta_type'], $validTypes)) {
            settype($value, $this->attributes['meta_type']);
        }

        return $value;
    }

    /**
     * Set key of the meta attribute.
     *
     * @param string $key
     *
     * @throws \InvalidArgumentException
     */
    protected function setMetaKey($key)
    {
        if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $key)) {
            throw new InvalidArgumentException("Provided key [{$key}] is not valid variable name.");
        }

        $this->attributes['meta_key'] = $key;
    }
    /**
     * Set group of the meta attribute.
     *
     * @param string $group
     *
     * @throws \InvalidArgumentException
     */
    public function setMetaGroup($group = null)
    {
        if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $group) && $group !== null) {
            throw new InvalidArgumentException("Provided group [{$group}] is not valid variable name.");
        }

        $this->attributes['meta_group'] = $group;
    }
    /**
     * Set type of the meta attribute.
     *
     * @param mixed $value
     */
    protected function setType($value)
    {
        $this->attributes['meta_type'] = $this->hasMutator($value, 'setter')
            ? $this->getMutatedType($value, 'setter')
            : $this->getValueType($value);
    }

    /**
     * Set value of the meta attribute.
     *
     * @param mixed $value
     *
     * @throws \Sofa\Eloquence\Metable\InvalidTypeException
     */
    public function setValue($value)
    {
        $this->setType($value);

        if ($this->hasMutator($value, 'setter')) {
            $value = $this->mutateValue($value, 'setter');
        } elseif (!$this->isStringable($value) && !is_null($value)) {
            throw new InvalidTypeException(
                "Unsupported meta value type [{$this->getValueType($value)}]."
            );
        }

        $this->attributes['meta_value'] = $value;
    }

    /**
     * Mutate attribute value.
     *
     * @param  mixed  $value
     * @param  string $dir
     * @return mixed
     */
    protected function mutateValue($value, $dir = 'setter')
    {
        $mutator = $this->getMutator($value, $dir, $this->attributes['meta_type']);

        if (method_exists($this, $mutator)) {
            return $this->{$mutator}($value);
        }

        return static::$attributeMutator->mutate($value, $mutator);
    }

    /**
     * Determine whether the value type can be set to string.
     *
     * @param  mixed   $value
     * @return boolean
     */
    protected function isStringable($value)
    {
        return is_scalar($value);
    }

    /**
     * Get the value type.
     *
     * @param  mixed  $value
     * @return string
     */
    protected function getValueType($value)
    {
        $type = is_object($value) ? get_class($value) : gettype($value);

        // use float instead of deprecated double
        return ($type == 'double') ? 'float' : $type;
    }

    /**
     * Get the mutated type.
     *
     * @param  mixed  $value
     * @param  string $dir
     * @return string
     */
    protected function getMutatedType($value, $dir = 'setter')
    {
        foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) {
            if ($this->getValueType($value) == $mutated || $value instanceof $mutated) {
                return $mutated;
            }
        }
    }

    /**
     * Determine whether a mutator exists for the value type.
     *
     * @param  mixed   $value
     * @param  string  $dir
     * @return boolean
     */
    protected function hasMutator($value, $dir = 'setter', $type = null)
    {
        return (bool) $this->getMutator($value, $dir, $type);
    }

    /**
     * Get mutator for the type.
     *
     * @param  mixed  $value
     * @param  string $dir
     * @return string
     */
    protected function getMutator($value, $dir = 'setter', $type = null)
    {
        $type = $type ?: $this->getValueType($value);

        foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) {
            if ($type == $mutated || $value instanceof $mutated) {
                return $mutator;
            }
        }
    }

    /**
     * Allow custom table name for meta attributes via config.
     *
     * @return string
     */
    public function getTable()
    {
        return isset(static::$customTable) ? static::$customTable : parent::getTable();
    }

    /**
     * Set custom table for the meta attributes. Allows doing it only once
     * in order to mimic protected behaviour, most likely in the service
     * provider, which in turn gets the table name from configuration.
     *
     * @param string $table
     */
    public static function setCustomTable($table)
    {
        if (!isset(static::$customTable)) {
            static::$customTable = $table;
        }
    }

    /**
     * Handle casting value to string.
     *
     * @return string
     */
    public function castToString()
    {
        if ($this->attributes['meta_type'] == 'array') {
            return $this->attributes['meta_value'];
        }

        $value = $this->getValue();

        if ($this->isStringable($value) || is_object($value) && method_exists($value, '__toString')) {
            return (string) $value;
        }

        return '';
    }

    /**
     * Handle dynamic casting to string.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->castToString();
    }
}