<?php
/**
 * @link http://www.yiiframework.com/
 *
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace Yiisoft\Rbac;

use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Yiisoft\Db\ConnectionInterface;
use Yiisoft\Db\Expression;
use Yiisoft\Db\Query;
use Yiisoft\Rbac\Exceptions\InvalidArgumentException;
use Yiisoft\Rbac\Exceptions\InvalidCallException;

/**
 * DbManager represents an authorization manager that stores authorization information in database.
 *
 * The database connection is specified by [[db]]. The database schema could be initialized by applying migration:
 *
 * ```
 * yii migrate --migrationPath=@yii/rbac/migrations/
 * ```
 *
 * If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
 *
 * You may change the names of the tables used to store the authorization and rule data by setting [[itemTable]],
 * [[itemChildTable]], [[assignmentTable]] and [[ruleTable]].
 *
 * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization).
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Alexander Kochetov <creocoder@gmail.com>
 *
 * @since 2.0
 */
class DbManager extends BaseManager
{
    /**
     * @var ConnectionInterface|array|string the DB connection object or the application component ID of the DB connection.
     *                                       After the DbManager object is created, if you want to change this property, you should only assign it
     *                                       with a DB connection object.
     *                                       Starting from version 2.0.2, this can also be a configuration array for creating the object.
     */
    public $db = 'db';
    /**
     * @var string the name of the table storing authorization items. Defaults to "auth_item".
     */
    public $itemTable = '{{%auth_item}}';
    /**
     * @var string the name of the table storing authorization item hierarchy. Defaults to "auth_item_child".
     */
    public $itemChildTable = '{{%auth_item_child}}';
    /**
     * @var string the name of the table storing authorization item assignments. Defaults to "auth_assignment".
     */
    public $assignmentTable = '{{%auth_assignment}}';
    /**
     * @var string the name of the table storing rules. Defaults to "auth_rule".
     */
    public $ruleTable = '{{%auth_rule}}';
    /**
     * @var CacheInterface|array|string the cache used to improve RBAC performance. This can be one of the following:
     *
     * - an application component ID (e.g. `cache`)
     * - a configuration array
     * - a [[\yii\caching\Cache]] object
     *
     * When this is not set, it means caching is not enabled.
     *
     * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
     * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
     * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
     * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
     *
     * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
     * you have to manually call [[invalidateCache()]] to ensure data consistency.
     *
     * @since 2.0.3
     */
    public $cache;
    /**
     * @var LoggerInterface
     */
    protected $logger;
    /**
     * @var string the key used to store RBAC data in cache
     *
     * @see cache
     * @since 2.0.3
     */
    public $cacheKey = 'rbac';

    /**
     * @var Item[] all auth items (name => Item)
     */
    protected $items;
    /**
     * @var Rule[] all auth rules (name => Rule)
     */
    protected $rules;
    /**
     * @var array auth item parent-child relationships (childName => list of parents)
     */
    protected $parents;

    /**
     * @param ConnectionInterface  $db
     * @param RuleFactoryInterface $ruleFactory
     */
    public function __construct(ConnectionInterface $db, RuleFactoryInterface $ruleFactory, ?CacheInterface $cache, ?LoggerInterface $logger)
    {
        parent::__construct($ruleFactory);
        $this->db = $db;
        $this->cache = $cache;
        $this->logger = $logger;
    }

    private $_checkAccessAssignments = [];

    /**
     * {@inheritdoc}
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        if (isset($this->_checkAccessAssignments[(string) $userId])) {
            $assignments = $this->_checkAccessAssignments[(string) $userId];
        } else {
            $assignments = $this->getAssignments($userId);
            $this->_checkAccessAssignments[(string) $userId] = $assignments;
        }

        if ($this->hasNoAssignments($assignments)) {
            return false;
        }

        $this->loadFromCache();
        if ($this->items !== null) {
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
        }

        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
    }

    /**
     * Performs access check for the specified user based on the data loaded from cache.
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
     *
     * @param string|int   $user        the user ID. This should can be either an integer or a string representing
     *                                  the unique identifier of a user. See [[\yii\web\User::id]].
     * @param string       $itemName    the name of the operation that need access check
     * @param array        $params      name-value pairs that would be passed to rules associated
     *                                  with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
     *                                  which holds the value of `$userId`.
     * @param Assignment[] $assignments the assignments to the specified user
     *
     * @return bool whether the operations can be performed by the user.
     *
     * @since 2.0.3
     */
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
    {
        if (!isset($this->items[$itemName])) {
            return false;
        }

        $item = $this->items[$itemName];

        $this->debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);

        if (!$this->executeRule($user, $item, $params)) {
            return false;
        }

        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
            return true;
        }

        if (!empty($this->parents[$itemName])) {
            foreach ($this->parents[$itemName] as $parent) {
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Performs access check for the specified user.
     * This method is internally called by [[checkAccess()]].
     *
     * @param string|int   $user        the user ID. This should can be either an integer or a string representing
     *                                  the unique identifier of a user. See [[\yii\web\User::id]].
     * @param string       $itemName    the name of the operation that need access check
     * @param array        $params      name-value pairs that would be passed to rules associated
     *                                  with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
     *                                  which holds the value of `$userId`.
     * @param Assignment[] $assignments the assignments to the specified user
     *
     * @return bool whether the operations can be performed by the user.
     */
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
    {
        if (($item = $this->getItem($itemName)) === null) {
            return false;
        }

        $this->debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);

        if (!$this->executeRule($user, $item, $params)) {
            return false;
        }

        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
            return true;
        }

        $query = new Query();
        $parents = $query->select(['parent'])
            ->from($this->itemChildTable)
            ->where(['child' => $itemName])
            ->column($this->db);
        foreach ($parents as $parent) {
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
                return true;
            }
        }

        return false;
    }

    protected function debug(string $message, string $method)
    {
        if ($this->logger) {
            $this->logger->debug($message, ['method' => $method]);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getItem($name)
    {
        if (empty($name)) {
            return;
        }

        if (!empty($this->items[$name])) {
            return $this->items[$name];
        }

        $row = (new Query())->from($this->itemTable)
            ->where(['name' => $name])
            ->one($this->db);

        if ($row === false) {
            return;
        }

        return $this->populateItem($row);
    }

    /**
     * Returns a value indicating whether the database supports cascading update and delete.
     * The default implementation will return false for SQLite database and true for all other databases.
     *
     * @return bool whether the database supports cascading update and delete.
     */
    protected function supportsCascadeUpdate()
    {
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
    }

    /**
     * {@inheritdoc}
     */
    protected function addItem($item)
    {
        $time = time();
        if ($item->createdAt === null) {
            $item->createdAt = $time;
        }
        if ($item->updatedAt === null) {
            $item->updatedAt = $time;
        }
        $this->db->createCommand()
            ->insert($this->itemTable, [
                'name'        => $item->name,
                'type'        => $item->type,
                'description' => $item->description,
                'rule_name'   => $item->ruleName,
                'data'        => $item->data === null ? null : serialize($item->data),
                'created_at'  => $item->createdAt,
                'updated_at'  => $item->updatedAt,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function removeItem($item)
    {
        if (!$this->supportsCascadeUpdate()) {
            $this->db->createCommand()
                ->delete($this->itemChildTable, ['or', '[[parent]]=:name', '[[child]]=:name'], [':name' => $item->name])
                ->execute();
            $this->db->createCommand()
                ->delete($this->assignmentTable, ['item_name' => $item->name])
                ->execute();
        }

        $this->db->createCommand()
            ->delete($this->itemTable, ['name' => $item->name])
            ->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function updateItem($name, $item)
    {
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
            $this->db->createCommand()
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
                ->execute();
            $this->db->createCommand()
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
                ->execute();
            $this->db->createCommand()
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
                ->execute();
        }

        $item->updatedAt = time();

        $this->db->createCommand()
            ->update($this->itemTable, [
                'name'        => $item->name,
                'description' => $item->description,
                'rule_name'   => $item->ruleName,
                'data'        => $item->data === null ? null : serialize($item->data),
                'updated_at'  => $item->updatedAt,
            ], [
                'name' => $name,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function addRule($rule)
    {
        $time = time();
        if ($rule->createdAt === null) {
            $rule->createdAt = $time;
        }
        if ($rule->updatedAt === null) {
            $rule->updatedAt = $time;
        }
        $this->db->createCommand()
            ->insert($this->ruleTable, [
                'name'       => $rule->name,
                'data'       => serialize($rule),
                'created_at' => $rule->createdAt,
                'updated_at' => $rule->updatedAt,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function updateRule($name, $rule)
    {
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
            $this->db->createCommand()
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
                ->execute();
        }

        $rule->updatedAt = time();

        $this->db->createCommand()
            ->update($this->ruleTable, [
                'name'       => $rule->name,
                'data'       => serialize($rule),
                'updated_at' => $rule->updatedAt,
            ], [
                'name' => $name,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function removeRule($rule)
    {
        if (!$this->supportsCascadeUpdate()) {
            $this->db->createCommand()
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
                ->execute();
        }

        $this->db->createCommand()
            ->delete($this->ruleTable, ['name' => $rule->name])
            ->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function getItems($type)
    {
        $query = (new Query())
            ->from($this->itemTable)
            ->where(['type' => $type]);

        $items = [];
        foreach ($query->all($this->db) as $row) {
            $items[$row['name']] = $this->populateItem($row);
        }

        return $items;
    }

    /**
     * Populates an auth item with the data fetched from database.
     *
     * @param array $row the data from the auth item table
     *
     * @return Item the populated auth item instance (either Role or Permission)
     */
    protected function populateItem($row)
    {
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::class : Role::class;

        if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
            $data = null;
        }

        return new $class([
            'name'        => $row['name'],
            'type'        => $row['type'],
            'description' => $row['description'],
            'ruleName'    => $row['rule_name'],
            'data'        => $data,
            'createdAt'   => $row['created_at'],
            'updatedAt'   => $row['updated_at'],
        ]);
    }

    /**
     * {@inheritdoc}
     * The roles returned by this method include the roles assigned via [[$defaultRoles]].
     */
    public function getRolesByUser($userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return [];
        }

        $query = (new Query())->select('b.*')
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
            ->andWhere(['a.user_id' => (string) $userId])
            ->andWhere(['b.type' => Item::TYPE_ROLE]);

        $roles = $this->getDefaultRoleInstances();
        foreach ($query->all($this->db) as $row) {
            $roles[$row['name']] = $this->populateItem($row);
        }

        return $roles;
    }

    /**
     * {@inheritdoc}
     */
    public function getChildRoles($roleName)
    {
        $role = $this->getRole($roleName);

        if ($role === null) {
            throw new InvalidArgumentException("Role \"$roleName\" not found.");
        }

        $result = [];
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);

        $roles = [$roleName => $role];

        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
            return array_key_exists($roleItem->name, $result);
        });

        return $roles;
    }

    /**
     * {@inheritdoc}
     */
    public function getPermissionsByRole($roleName)
    {
        $childrenList = $this->getChildrenList();
        $result = [];
        $this->getChildrenRecursive($roleName, $childrenList, $result);
        if (empty($result)) {
            return [];
        }
        $query = (new Query())->from($this->itemTable)->where([
            'type' => Item::TYPE_PERMISSION,
            'name' => array_keys($result),
        ]);
        $permissions = [];
        foreach ($query->all($this->db) as $row) {
            $permissions[$row['name']] = $this->populateItem($row);
        }

        return $permissions;
    }

    /**
     * {@inheritdoc}
     */
    public function getPermissionsByUser($userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return [];
        }

        $directPermission = $this->getDirectPermissionsByUser($userId);
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);

        return array_merge($directPermission, $inheritedPermission);
    }

    /**
     * Returns all permissions that are directly assigned to user.
     *
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
     *
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
     *
     * @since 2.0.7
     */
    protected function getDirectPermissionsByUser($userId)
    {
        $query = (new Query())->select('b.*')
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
            ->andWhere(['a.user_id' => (string) $userId])
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);

        $permissions = [];
        foreach ($query->all($this->db) as $row) {
            $permissions[$row['name']] = $this->populateItem($row);
        }

        return $permissions;
    }

    /**
     * Returns all permissions that the user inherits from the roles assigned to him.
     *
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
     *
     * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
     *
     * @since 2.0.7
     */
    protected function getInheritedPermissionsByUser($userId)
    {
        $query = (new Query())->select('item_name')
            ->from($this->assignmentTable)
            ->where(['user_id' => (string) $userId]);

        $childrenList = $this->getChildrenList();
        $result = [];
        foreach ($query->column($this->db) as $roleName) {
            $this->getChildrenRecursive($roleName, $childrenList, $result);
        }

        if (empty($result)) {
            return [];
        }

        $query = (new Query())->from($this->itemTable)->where([
            'type' => Item::TYPE_PERMISSION,
            'name' => array_keys($result),
        ]);
        $permissions = [];
        foreach ($query->all($this->db) as $row) {
            $permissions[$row['name']] = $this->populateItem($row);
        }

        return $permissions;
    }

    /**
     * Returns the children for every parent.
     *
     * @return array the children list. Each array key is a parent item name,
     *               and the corresponding array value is a list of child item names.
     */
    protected function getChildrenList()
    {
        $query = (new Query())->from($this->itemChildTable);
        $parents = [];
        foreach ($query->all($this->db) as $row) {
            $parents[$row['parent']][] = $row['child'];
        }

        return $parents;
    }

    /**
     * Recursively finds all children and grand children of the specified item.
     *
     * @param string $name         the name of the item whose children are to be looked for.
     * @param array  $childrenList the child list built via [[getChildrenList()]]
     * @param array  $result       the children and grand children (in array keys)
     */
    protected function getChildrenRecursive($name, $childrenList, &$result)
    {
        if (isset($childrenList[$name])) {
            foreach ($childrenList[$name] as $child) {
                $result[$child] = true;
                $this->getChildrenRecursive($child, $childrenList, $result);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getRule($name)
    {
        if ($this->rules !== null) {
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
        }

        $row = (new Query())->select(['data'])
            ->from($this->ruleTable)
            ->where(['name' => $name])
            ->one($this->db);
        if ($row === false) {
            return;
        }
        $data = $row['data'];
        if (is_resource($data)) {
            $data = stream_get_contents($data);
        }

        return unserialize($data);
    }

    /**
     * {@inheritdoc}
     */
    public function getRules()
    {
        if ($this->rules !== null) {
            return $this->rules;
        }

        $query = (new Query())->from($this->ruleTable);

        $rules = [];
        foreach ($query->all($this->db) as $row) {
            $data = $row['data'];
            if (is_resource($data)) {
                $data = stream_get_contents($data);
            }
            $rules[$row['name']] = unserialize($data);
        }

        return $rules;
    }

    /**
     * {@inheritdoc}
     */
    public function getAssignment($roleName, $userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return;
        }

        $row = (new Query())->from($this->assignmentTable)
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
            ->one($this->db);

        if ($row === false) {
            return;
        }

        return new Assignment([
            'userId'    => $row['user_id'],
            'roleName'  => $row['item_name'],
            'createdAt' => $row['created_at'],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getAssignments($userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return [];
        }

        $query = (new Query())
            ->from($this->assignmentTable)
            ->where(['user_id' => (string) $userId]);

        $assignments = [];
        foreach ($query->all($this->db) as $row) {
            $assignments[$row['item_name']] = new Assignment([
                'userId'    => $row['user_id'],
                'roleName'  => $row['item_name'],
                'createdAt' => $row['created_at'],
            ]);
        }

        return $assignments;
    }

    /**
     * {@inheritdoc}
     *
     * @since 2.0.8
     */
    public function canAddChild($parent, $child)
    {
        return !$this->detectLoop($parent, $child);
    }

    /**
     * {@inheritdoc}
     */
    public function addChild($parent, $child)
    {
        if ($parent->name === $child->name) {
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
        }

        if ($parent instanceof Permission && $child instanceof Role) {
            throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
        }

        if ($this->detectLoop($parent, $child)) {
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
        }

        $this->db->createCommand()
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
            ->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function removeChild($parent, $child)
    {
        $result = $this->db->createCommand()
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
            ->execute() > 0;

        $this->invalidateCache();

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function removeChildren($parent)
    {
        $result = $this->db->createCommand()
            ->delete($this->itemChildTable, ['parent' => $parent->name])
            ->execute() > 0;

        $this->invalidateCache();

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function hasChild($parent, $child)
    {
        return (new Query())
            ->from($this->itemChildTable)
            ->where(['parent' => $parent->name, 'child' => $child->name])
            ->one($this->db) !== false;
    }

    /**
     * {@inheritdoc}
     */
    public function getChildren($name)
    {
        $query = (new Query())
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
            ->from([$this->itemTable, $this->itemChildTable])
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);

        $children = [];
        foreach ($query->all($this->db) as $row) {
            $children[$row['name']] = $this->populateItem($row);
        }

        return $children;
    }

    /**
     * Checks whether there is a loop in the authorization item hierarchy.
     *
     * @param Item $parent the parent item
     * @param Item $child  the child item to be added to the hierarchy
     *
     * @return bool whether a loop exists
     */
    protected function detectLoop($parent, $child)
    {
        if ($child->name === $parent->name) {
            return true;
        }
        foreach ($this->getChildren($child->name) as $grandchild) {
            if ($this->detectLoop($parent, $grandchild)) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function assign($role, $userId)
    {
        $assignment = new Assignment([
            'userId'    => $userId,
            'roleName'  => $role->name,
            'createdAt' => time(),
        ]);

        $this->db->createCommand()
            ->insert($this->assignmentTable, [
                'user_id'    => $assignment->userId,
                'item_name'  => $assignment->roleName,
                'created_at' => $assignment->createdAt,
            ])->execute();

        unset($this->_checkAccessAssignments[(string) $userId]);

        return $assignment;
    }

    /**
     * {@inheritdoc}
     */
    public function revoke($role, $userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return false;
        }

        unset($this->_checkAccessAssignments[(string) $userId]);

        return $this->db->createCommand()
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
            ->execute() > 0;
    }

    /**
     * {@inheritdoc}
     */
    public function revokeAll($userId)
    {
        if ($this->isEmptyUserId($userId)) {
            return false;
        }

        unset($this->_checkAccessAssignments[(string) $userId]);

        return $this->db->createCommand()
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
            ->execute() > 0;
    }

    /**
     * {@inheritdoc}
     */
    public function removeAll()
    {
        $this->removeAllAssignments();
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
        $this->db->createCommand()->delete($this->itemTable)->execute();
        $this->db->createCommand()->delete($this->ruleTable)->execute();
        $this->invalidateCache();
    }

    /**
     * {@inheritdoc}
     */
    public function removeAllPermissions()
    {
        $this->removeAllItems(Item::TYPE_PERMISSION);
    }

    /**
     * {@inheritdoc}
     */
    public function removeAllRoles()
    {
        $this->removeAllItems(Item::TYPE_ROLE);
    }

    /**
     * Removes all auth items of the specified type.
     *
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
     */
    protected function removeAllItems($type)
    {
        if (!$this->supportsCascadeUpdate()) {
            $names = (new Query())
                ->select(['name'])
                ->from($this->itemTable)
                ->where(['type' => $type])
                ->column($this->db);
            if (empty($names)) {
                return;
            }
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
            $this->db->createCommand()
                ->delete($this->itemChildTable, [$key => $names])
                ->execute();
            $this->db->createCommand()
                ->delete($this->assignmentTable, ['item_name' => $names])
                ->execute();
        }
        $this->db->createCommand()
            ->delete($this->itemTable, ['type' => $type])
            ->execute();

        $this->invalidateCache();
    }

    /**
     * {@inheritdoc}
     */
    public function removeAllRules()
    {
        if (!$this->supportsCascadeUpdate()) {
            $this->db->createCommand()
                ->update($this->itemTable, ['rule_name' => null])
                ->execute();
        }

        $this->db->createCommand()->delete($this->ruleTable)->execute();

        $this->invalidateCache();
    }

    /**
     * {@inheritdoc}
     */
    public function removeAllAssignments()
    {
        $this->_checkAccessAssignments = [];
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
    }

    public function invalidateCache()
    {
        if ($this->cache !== null) {
            $this->cache->delete($this->cacheKey);
            $this->items = null;
            $this->rules = null;
            $this->parents = null;
        }
        $this->_checkAccessAssignments = [];
    }

    public function loadFromCache()
    {
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
            return;
        }

        $data = $this->cache->get($this->cacheKey);
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
            [$this->items, $this->rules, $this->parents] = $data;

            return;
        }

        $query = (new Query())->from($this->itemTable);
        $this->items = [];
        foreach ($query->all($this->db) as $row) {
            $this->items[$row['name']] = $this->populateItem($row);
        }

        $query = (new Query())->from($this->ruleTable);
        $this->rules = [];
        foreach ($query->all($this->db) as $row) {
            $data = $row['data'];
            if (is_resource($data)) {
                $data = stream_get_contents($data);
            }
            $this->rules[$row['name']] = unserialize($data);
        }

        $query = (new Query())->from($this->itemChildTable);
        $this->parents = [];
        foreach ($query->all($this->db) as $row) {
            if (isset($this->items[$row['child']])) {
                $this->parents[$row['child']][] = $row['parent'];
            }
        }

        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
    }

    /**
     * Returns all role assignment information for the specified role.
     *
     * @param string $roleName
     *
     * @return string[] the ids. An empty array will be
     *                  returned if role is not assigned to any user.
     *
     * @since 2.0.7
     */
    public function getUserIdsByRole($roleName)
    {
        if (empty($roleName)) {
            return [];
        }

        return (new Query())->select('[[user_id]]')
            ->from($this->assignmentTable)
            ->where(['item_name' => $roleName])->column($this->db);
    }

    /**
     * Check whether $userId is empty.
     *
     * @param mixed $userId
     *
     * @return bool
     */
    private function isEmptyUserId($userId)
    {
        return !isset($userId) || $userId === '';
    }
}


<?php

namespace yii2mod\cart;

use Yii;
use yii\base\Component;
use yii\base\InvalidParamException;
use yii2mod\cart\models\CartItemInterface;
use yii2mod\cart\storage\StorageInterface;

/**
 * Class Cart provides basic cart functionality (adding, removing, clearing, listing items). You can extend this class and
 * override it in the application configuration to extend/customize the functionality
 *
 * @package yii2mod\cart
 */
class Cart extends Component
{
    /**
     * @var string CartItemInterface class name
     */
    const ITEM_PRODUCT = '\yii2mod\cart\models\CartItemInterface';

    /**
     * Override this to provide custom (e.g. database) storage for cart data
     *
     * @var string|\yii2mod\cart\storage\StorageInterface
     */
    public $storageClass = '\yii2mod\cart\storage\SessionStorage';

    /**
     * @var array cart items
     */
    protected $items;

    /**
     * @var StorageInterface
     */
    private $_storage;

    /**
     * @inheritdoc
     */
    public function init()
    {
        $this->clear(false);
        $this->setStorage(Yii::createObject($this->storageClass));
        $this->items = $this->storage->load($this);
    }

    /**
     * Assigns cart to logged in user
     *
     * @param string
     * @param string
     */
    public function reassign($sessionId, $userId)
    {
        if (get_class($this->getStorage()) === 'yii2mod\cart\storage\DatabaseStorage') {
            if (!empty($this->items)) {
                $storage = $this->getStorage();
                $storage->reassign($sessionId, $userId);
                self::init();
            }
        }
    }

    /**
     * Delete all items from the cart
     *
     * @param bool $save
     *
     * @return $this
     */
    public function clear($save = true): self
    {
        $this->items = [];
        $save && $this->storage->save($this);

        return $this;
    }

    /**
     * @return StorageInterface
     */
    public function getStorage(): StorageInterface
    {
        return $this->_storage;
    }

    /**
     * @param mixed $storage
     */
    public function setStorage($storage)
    {
        $this->_storage = $storage;
    }

    /**
     * Add an item to the cart
     *
     * @param models\CartItemInterface $element
     * @param bool $save
     *
     * @return $this
     */
    public function add(CartItemInterface $element, $save = true): self
    {
        $this->addItem($element);
        $save && $this->storage->save($this);

        return $this;
    }

    /**
     * @param \yii2mod\cart\models\CartItemInterface $item
     */
    protected function addItem(CartItemInterface $item)
    {
        $uniqueId = $item->getUniqueId();
        $this->items[$uniqueId] = $item;
    }

    /**
     * Removes an item from the cart
     *
     * @param string $uniqueId
     * @param bool $save
     *
     * @throws \yii\base\InvalidParamException
     *
     * @return $this
     */
    public function remove($uniqueId, $save = true): self
    {
        if (!isset($this->items[$uniqueId])) {
            throw new InvalidParamException('Item not found');
        }

        unset($this->items[$uniqueId]);

        $save && $this->storage->save($this);

        return $this;
    }

    /**
     * @param string $itemType If specified, only items of that type will be counted
     *
     * @return int
     */
    public function getCount($itemType = null): int
    {
        return count($this->getItems($itemType));
    }

    /**
     * Returns all items of a given type from the cart
     *
     * @param string $itemType One of self::ITEM_ constants
     *
     * @return CartItemInterface[]
     */
    public function getItems($itemType = null): array
    {
        $items = $this->items;

        if (!is_null($itemType)) {
            $items = array_filter(
                $items,
                function ($item) use ($itemType) {
                    /* @var $item CartItemInterface */
                    return is_a($item, $itemType);
                }
            );
        }

        return $items;
    }

    /**
     * Finds all items of type $itemType, sums the values of $attribute of all models and returns the sum.
     *
     * @param string $attribute
     * @param string|null $itemType
     *
     * @return int
     */
    public function getAttributeTotal($attribute, $itemType = null): int
    {
        $sum = 0;
        foreach ($this->getItems($itemType) as $model) {
            $sum += $model->{$attribute};
        }

        return $sum;
    }
}


<?php

namespace yii2mod\cart\storage;

use Yii;
use yii\base\BaseObject;
use yii\base\InvalidConfigException;
use yii\db\Connection;
use yii\db\Query;
use yii\web\User;
use yii2mod\cart\Cart;

/**
 * Class DatabaseStorage is a database adapter for cart data storage.
 *
 * If userComponent is set, it tries to call getId() from the component and use the result as user identifier. If it
 * fails, or if $userComponent is not set, it will use sessionId as user identifier
 *
 * @package yii2mod\cart\storage
 */
class DatabaseStorage extends BaseObject implements StorageInterface
{
    /**
     * @var string Name of the user component
     */
    public $userComponent = 'user';

    /**
     * @var string Name of the database component
     */
    public $dbComponent = 'db';

    /**
     * @var string Name of the cart table
     */
    public $table = '{{%cart}}';

    /**
     * @var string Name of the
     */
    public $idField = 'sessionId';

    /**
     * @var string Name of the field holding serialized session data
     */
    public $dataField = 'cartData';

    /**
     * @var bool If set to true, empty cart entries will be deleted
     */
    public $deleteIfEmpty = false;

    /**
     * @var Connection
     */
    private $_db;

    /**
     * @var User
     */
    private $_user;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        $this->_db = Yii::$app->get($this->dbComponent);

        if ($this->userComponent !== null) {
            $this->_user = Yii::$app->get($this->userComponent);
        }

        if ($this->table === null) {
            throw new InvalidConfigException('Please specify "table" in cart configuration');
        }
    }

    /**
     * @param Cart $cart
     *
     * @return mixed
     */
    public function load(Cart $cart)
    {
        $items = [];
        $identifier = $this->getIdentifier(Yii::$app->session->getId());

        $query = new Query();
        $query->select($this->dataField)
            ->from($this->table)
            ->where([$this->idField => $identifier]);

        if ($data = $query->createCommand($this->_db)->queryScalar()) {
            $items = unserialize($data);
        }

        return $items;
    }

    /**
     * @param int $default
     *
     * @return int
     */
    protected function getIdentifier($default)
    {
        $id = $default;

        if ($this->_user instanceof User && !$this->_user->getIsGuest()) {
            $id = $this->_user->getId();
        }

        return $id;
    }

    /**
     * @param \yii2mod\cart\Cart $cart
     */
    public function save(Cart $cart)
    {
        $identifier = $this->getIdentifier(Yii::$app->session->getId());

        $items = $cart->getItems();
        $sessionData = serialize($items);

        $command = $this->_db->createCommand();

        if (empty($items) && true === $this->deleteIfEmpty) {
            $command->delete($this->table, [$this->idField => $identifier]);
        } else {
            $command->setSql("
                REPLACE {{{$this->table}}}
                SET
                    {{{$this->dataField}}} = :val,
                    {{{$this->idField}}} = :id
            ")->bindValues([
                ':id' => $identifier,
                ':val' => $sessionData,
            ]);
        }

        $command->execute();
    }

    /**
     * Assigns cart to logged in user
     *
     * @param $sourceId
     * @param $destinationId
     */
    public function reassign($sourceId, $destinationId)
    {
        $command = $this->_db->createCommand();

        $command->delete($this->table, [$this->idField => $destinationId])->execute();

        $command->update($this->table, [$this->idField => $destinationId], [$this->idField => $sourceId])->execute();
    }
}


<?php

/**
  * @author Conekta
  * @author Conekta Developers <soporte@conekta.com>
  */

namespace Conekta;

use \Conekta\Conekta;
use \Conekta\Handler;

/**
  * @method string  apiUrl($urls string)
  * @method array() additionalPluginHeaders()
  * @method array() setHeaders()
  * @method json    request($method string, $url string, $params array())
  * @method string  buildQueryParamsUrl($url string, $params arraay())
  * @method string  buildSegmentParamsUrl($url string, $params arraay())
  */
class Requestor
{
  public $apiKey;

  public function __construct()
  {
    $this->apiKey = Conekta::$apiKey;
    $this->apiVersion = Conekta::$apiVersion;
    $this->plugin = Conekta::$plugin;
  }

  /**
   * Function apiUrl
   *
   * get Base path of conekta api i.e. https://api.conekta.com
   *
   * @param url (string) endpoint to concatenate
   * @return (string)
   */
  public static function apiUrl($url = '')
  {
    $apiBase = Conekta::$apiBase;

    return $apiBase . $url;
  }

  /**
   * Function additionalPluginHeaders
   *
   * Set headers if is plugin implementation
   *
   * @return (array)
   */
  private function additionalPluginHeaders()
  {
    return array(
      'plugin_name' => Conekta::getPlugin(),
      'plugin_version' => Conekta::getPluginVersion()
    );
  }

  /**
   * Function setHeaders
   *
   * Set Standar headers for library
   *
   * @return (array)
   */
  private function setHeaders()
  {
    $pluginAgent = $this->additionalPluginHeaders();
    $userAgent = array(
      'bindings_version' => Conekta::VERSION,
      'lang'             => 'php',
      'lang_version'     => phpversion(),
      'publisher'        => 'conekta',
      'uname'            => php_uname(),
      );

    if(array_filter($pluginAgent)){
      $userAgent = array_merge($userAgent, $pluginAgent);
    }

    $headers = array(
      'Accept: application/vnd.conekta-v'.Conekta::$apiVersion.'+json',
      'Accept-Language: '.Conekta::$locale,
      'X-Conekta-Client-User-Agent: '.json_encode($userAgent),
      'User-Agent: Conekta/v1 PhpBindings/'.Conekta::VERSION,
      'Authorization: Basic '.base64_encode($this->apiKey.':'),
      'Content-Type: application/json'
      );

    return $headers;
  }

  /**
   * Function request
   *
   * Make api call
   *
   * @param method (string) REST action [DELETE,PUT,POST,GET]
   * @param url (string) endpoint to concatenate
   * @param params (array) contains body request
   * @return (json)
   */
  public function request($method, $url, $params = null)
  {
    $jsonParams = json_encode($params);
    $headers = $this->setHeaders();
    $curl = curl_init();
    $method = strtolower($method);
    $opts = array();

    switch ($method) {
      case 'get':
      $opts[CURLOPT_HTTPGET] = 1;
      $url = $this->buildQueryParamsUrl($url, $params);
      break;
      case 'post':
      $opts[CURLOPT_POST] = 1;
      $opts[CURLOPT_POSTFIELDS] = $jsonParams;
      break;
      case 'put':
      $opts[CURLOPT_RETURNTRANSFER] = 1;
      $opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
      $opts[CURLOPT_POSTFIELDS] = $jsonParams;
      break;
      case 'delete':
      $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
      $url = $this->buildSegmentParamsUrl($url, $params);
      break;
      default:
      throw new \Exception('Wrong method');
    }

    $url = $this->apiUrl($url);
    $opts[CURLOPT_URL] = $url;
    $opts[CURLOPT_RETURNTRANSFER] = true;
    $opts[CURLOPT_CONNECTTIMEOUT] = 30;
    $opts[CURLOPT_TIMEOUT] = 80;
    $opts[CURLOPT_RETURNTRANSFER] = true;
    $opts[CURLOPT_HTTPHEADER] = $headers;
    $opts[CURLOPT_SSLVERSION] = 6;
    $opts[CURLOPT_CAINFO] = dirname(__FILE__).'/../ssl_data/ca_bundle.crt';
    curl_setopt_array($curl, $opts);
    $response = curl_exec($curl);
    $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    $jsonResponse = json_decode($response, true);

    if ($responseCode != 200) {
      throw Handler::errorHandler($jsonResponse, $responseCode);
    }

    return $jsonResponse;
  }

  /**
   * Function buildQueryParamsUrl
   *
   * build body request into url
   *
   * @param url (string) endpoint to concatenate
   * @param params (array) contains body request
   * @return (string)
   */
  private function buildQueryParamsUrl($url, $params)
  {
    if(!is_null($params)){
      $params = http_build_query($params);
      $url = $url.'?'.$params;
    }

    return $url;
  }

  /**
   * Function buildSegmentParamsUrl
   *
   * build body request for DELETE  action
   *
   * @param url (string) endpoint to concatenate
   * @param params (array) contains body request
   * @return (string)
   */
  private function buildSegmentParamsUrl($url, $params)
  {
    if(!is_array($params)){
      $url = $url.urlencode($params);
    }

    return $url;
  }
}


<?php

namespace Auth0\Login;

use Config;
use Auth0\SDK\API\Authentication;
use Auth0\SDK\Auth0;
use Auth0\SDK\JWTVerifier;
use Illuminate\Contracts\Container\BindingResolutionException;

/**
 * Service that provides access to the Auth0 SDK.
 */
class Auth0Service
{
    private $auth0Config;
    private $auth0;
    private $authApi;
    private $apiuser;
    private $_onLoginCb = null;
    private $rememberUser = false;

    public function __construct() {
      $this->auth0Config = config('laravel-auth0');

      $this->auth0Config['store'] = new LaravelSessionStore();

      $this->authApi = new Authentication($this->auth0Config['domain'], $this->auth0Config['client_id']);

      $this->auth0 = new Auth0($this->auth0Config);
    }

    /**
     * Creates an instance of the Auth0 SDK using
     * the config set in the laravel way and using a LaravelSession
     * as a store mechanism.
     */
    private function getSDK()
    {
        return $this->auth0;
    }

    /**
     * Logs the user out from the SDK.
     */
    public function logout()
    {
        $this->getSDK()->logout();
    }

    /**
     * Redirects the user to the hosted login page
     */
    public function login($connection = null, $state = null, $additional_params = ['scope' => 'openid profile email'], $response_type = 'code')
    {
        $additional_params['response_type'] = $response_type;
        $this->auth0->login($state, $connection, $additional_params);
    }

    /**
     * If the user is logged in, returns the user information.
     *
     * @return array with the User info as described in https://docs.auth0.com/user-profile and the user access token
     */
    public function getUser()
    {
        // Get the user info from auth0
        $auth0 = $this->getSDK();
        $user = $auth0->getUser();

        if ($user === null) {
            return;
        }

        return [
            'profile' => $user,
            'accessToken' => $auth0->getAccessToken(),
        ];
    }

    /**
     * Sets a callback to be called when the user is logged in.
     *
     * @param callback $cb A function that receives an auth0User and receives a Laravel user
     */
    public function onLogin($cb)
    {
        $this->_onLoginCb = $cb;
    }

    /**
     * @return bool
     */
    public function hasOnLogin()
    {
        return $this->_onLoginCb !== null;
    }

    /**
     * @param $auth0User
     *
     * @return mixed
     */
    public function callOnLogin($auth0User)
    {
        return call_user_func($this->_onLoginCb, $auth0User);
    }

    /**
     * Use this to either enable or disable the "remember" function for users.
     *
     * @param null $value
     *
     * @return bool|null
     */
    public function rememberUser($value = null)
    {
        if ($value !== null) {
            $this->rememberUser = $value;
        }

        return $this->rememberUser;
    }

    /**
     * @param $encUser
     *
     * @return mixed
     */
    public function decodeJWT($encUser)
    {
        try {
            $cache = \App::make('\Auth0\SDK\Helpers\Cache\CacheHandler');
        } catch (BindingResolutionException $e) {
            $cache = null;
        }

        $secret_base64_encoded = config('laravel-auth0.secret_base64_encoded');

        if (is_null($secret_base64_encoded)) {
          $secret_base64_encoded = true;
        }

        $verifier = new JWTVerifier([
            'valid_audiences' => [config('laravel-auth0.client_id'), config('laravel-auth0.api_identifier')],
            'supported_algs' => config('laravel-auth0.supported_algs', ['HS256']),
            'client_secret' => config('laravel-auth0.client_secret'),
            'authorized_iss' => config('laravel-auth0.authorized_issuers'),
            'secret_base64_encoded' => $secret_base64_encoded,
            'cache' => $cache,
            'guzzle_options' => config('laravel-auth0.guzzle_options'),
        ]);

        $this->apiuser = $verifier->verifyAndDecode($encUser);

        return $this->apiuser;
    }

    public function getIdToken()
    {
        return $this->getSDK()->getIdToken();
    }

    public function getAccessToken()
    {
        return $this->getSDK()->getAccessToken();
    }

    public function getRefreshToken()
    {
        return $this->getSDK()->getRefreshToken();
    }

    public function jwtuser()
    {
        return $this->apiuser;
    }
}


<?php

namespace Auth0\Login;

use Auth0\Login\Contract\Auth0UserRepository;
use Illuminate\Routing\Controller;

class Auth0Controller extends Controller
{
    /**
     * @var Auth0UserRepository
     */
    protected $userRepository;

    /**
     * Auth0Controller constructor.
     *
     * @param Auth0UserRepository $userRepository
     */
    public function __construct(Auth0UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Callback action that should be called by auth0, logs the user in.
     */
    public function callback()
    {
        // Get a handle of the Auth0 service (we don't know if it has an alias)
        $service = \App::make('auth0');

        // Try to get the user information
        $profile = $service->getUser();

        // Get the user related to the profile
        $auth0User = $this->userRepository->getUserByUserInfo($profile);

        if ($auth0User) {
            // If we have a user, we are going to log them in, but if
            // there is an onLogin defined we need to allow the Laravel developer
            // to implement the user as they want an also let them store it.
            if ($service->hasOnLogin()) {
                $user = $service->callOnLogin($auth0User);
            } else {
                // If not, the user will be fine
                $user = $auth0User;
            }
            \Auth::login($user, $service->rememberUser());
        }

        return \Redirect::intended('/');
    }
}


<?php

namespace Auth0\Login;

use Session;
use Auth0\SDK\Store\StoreInterface;

class LaravelSessionStore implements StoreInterface
{
    const BASE_NAME = 'auth0_';

    /**
     * Persists $value on $_SESSION, identified by $key.
     *
     * @see Auth0SDK\BaseAuth0
     *
     * @param string $key
     * @param mixed  $value
     */
    public function set($key, $value)
    {
        $key_name = $this->getSessionKeyName($key);
        Session::put($key_name, $value);
    }

    /**
     * @param $key
     * @param null $default
     *
     * @return mixed
     */
    public function get($key, $default = null)
    {
        $key_name = $this->getSessionKeyName($key);

        return Session::get($key_name, $default);
    }

    /**
     * Removes a persisted value identified by $key.
     *
     * @see Auth0SDK\BaseAuth0
     *
     * @param string $key
     */
    public function delete($key)
    {
        $key_name = $this->getSessionKeyName($key);

        Session::forget($key_name);
    }

    /**
     * Constructs a session var name.
     *
     * @param string $key
     *
     * @return string
     */
    public function getSessionKeyName($key)
    {
        return self::BASE_NAME.'_'.$key;
    }
}


<?php

namespace Auth0\Login;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Auth0\Login\Contract\Auth0UserRepository;
use Auth0\SDK\Exception\CoreException;
use Auth0\SDK\Exception\InvalidTokenException;

/**
 * Service that provides an Auth0\LaravelAuth0\Auth0User stored in the session. This User provider
 * should be used when you don't want to persist the entity.
 */
class Auth0UserProvider implements UserProvider
{
    protected $userRepository;
    protected $auth0;

    /**
     * Auth0UserProvider constructor.
     *
     * @param Auth0UserRepository       $userRepository
     * @param \Auth0\Login\Auth0Service $auth0
     */
    public function __construct(Auth0UserRepository $userRepository, Auth0Service $auth0)
    {
        $this->userRepository = $userRepository;
        $this->auth0 = $auth0;
    }

    /**
     * Lets make the repository take care of returning the user related to the
     * identifier.
     *
     * @param mixed $identifier
     *
     * @return Authenticatable
     */
    public function retrieveByID($identifier)
    {
        return $this->userRepository->getUserByIdentifier($identifier);
    }

    /**
     * @param array $credentials
     *
     * @return bool|Authenticatable
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (!isset($credentials['api_token'])) {
            return false;
        }

        $encUser = $credentials['api_token'];

        try {
            $decodedJWT = $this->auth0->decodeJWT($encUser);
        } catch (CoreException $e) {
            return null;
        } catch (InvalidTokenException $e) {
            return null;
        }

        return $this->userRepository->getUserByDecodedJWT($decodedJWT);
    }

    /**
     * Required method by the UserProviderInterface, we don't implement it.
     */
    public function retrieveByToken($identifier, $token)
    {
        return false;
    }

    /**
     * Required method by the UserProviderInterface, we don't implement it.
     */
    public function updateRememberToken(Authenticatable $user, $token)
    {
    }

    /**
     * Required method by the UserProviderInterface, we don't implement it.
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return false;
    }
}


<?php

namespace Auth0\Login;

use Illuminate\Support\ServiceProvider;
use Auth0\SDK\API\Helpers\ApiClient;
use Auth0\SDK\API\Helpers\InformationHeaders;

class LoginServiceProvider extends ServiceProvider {

    const SDK_VERSION = "4.0.4";

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

    /**
     * Bootstrap the application events.
     */
    public function boot()
    {
        \Auth::provider('auth0', function ($app, array $config) {
            return $app->make(Auth0UserProvider::class);
        });

        $this->publishes([
            __DIR__.'/../../config/config.php' => config_path('laravel-auth0.php'),
        ]);

        $laravel = app();

        $oldInfoHeaders = ApiClient::getInfoHeadersData();

        if ($oldInfoHeaders) {
            $infoHeaders = InformationHeaders::Extend($oldInfoHeaders);

            $infoHeaders->setEnvironment('Laravel', $laravel::VERSION);
            $infoHeaders->setPackage('laravel-auth0', self::SDK_VERSION);

            ApiClient::setInfoHeadersData($infoHeaders);
        }
    }

    /**
     * Register the service provider.
     */
    public function register()
    {
        // Bind the auth0 name to a singleton instance of the Auth0 Service
        $this->app->singleton(Auth0Service::class, function () {
              return new Auth0Service();
        });
        $this->app->singleton('auth0', function () {
              return $this->app->make(Auth0Service::class);
        });

        // When Laravel logs out, logout the auth0 SDK trough the service
        \Event::listen('auth.logout', function () {
            \App::make('auth0')->logout();
        });
        \Event::listen('user.logout', function () {
            \App::make('auth0')->logout();
        });
        \Event::listen('Illuminate\Auth\Events\Logout', function () {
            \App::make('auth0')->logout();
        });
    }

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


<?php

namespace Auth0\Login\Repository;

use Auth0\Login\Auth0User;
use Auth0\Login\Auth0JWTUser;
use Auth0\Login\Contract\Auth0UserRepository as Auth0UserRepositoryContract;

class Auth0UserRepository implements Auth0UserRepositoryContract
{
    /**
     * @param \Auth0\Login\Contract\stdClass $jwt
     *
     * @return Auth0JWTUser
     */
    public function getUserByDecodedJWT($jwt)
    {
        return new Auth0JWTUser($jwt);
    }

    /**
     * @param array $userInfo
     *
     * @return Auth0User
     */
    public function getUserByUserInfo($userInfo)
    {
        return new Auth0User($userInfo['profile'], $userInfo['accessToken']);
    }

    /**
     * @param \Auth0\Login\Contract\the $identifier
     *
     * @return Auth0User|\Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function getUserByIdentifier($identifier)
    {
        // Get the user info of the user logged in (probably in session)
        $user = \App::make('auth0')->getUser();

        if ($user === null) {
            return;
        }

        // Build the user
        $auth0User = $this->getUserByUserInfo($user);

        // It is not the same user as logged in, it is not valid
        if ($auth0User && $auth0User->getAuthIdentifier() == $identifier) {
            return $auth0User;
        }
    }
}


<?php
namespace morphos\Russian;

use morphos\S;

/**
 * Rules are from: http://www.imena.org/decl_mn.html / http://www.imena.org/decl_fn.html
 * and http://rus.omgpu.ru/2016/04/18/%D1%81%D0%BA%D0%BB%D0%BE%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BB%D0%B8%D1%87%D0%BD%D1%8B%D1%85-%D0%B8%D0%BC%D1%91%D0%BD/
 */
class FirstNamesInflection extends \morphos\NamesInflection implements Cases
{
    use RussianLanguage, CasesHelper;

    protected static $exceptions = [
        'лев' => [
            self::IMENIT => 'Лев',
            self::RODIT => 'Льва',
            self::DAT => 'Льву',
            self::VINIT => 'Льва',
            self::TVORIT => 'Львом',
            self::PREDLOJ => 'Льве',
        ],
        'павел' => [
            self::IMENIT => 'Павел',
            self::RODIT => 'Павла',
            self::DAT => 'Павлу',
            self::VINIT => 'Павла',
            self::TVORIT => 'Павлом',
            self::PREDLOJ => 'Павле',
        ]
    ];

    protected static $menNames = [
        'абрам', 'аверьян', 'авраам', 'агафон', 'адам', 'азар', 'акакий', 'аким', 'аксён', 'александр', 'алексей',
        'альберт', 'анатолий', 'андрей', 'андрон', 'антип', 'антон', 'аполлон', 'аристарх', 'аркадий', 'арнольд',
        'арсений', 'арсентий', 'артем', 'артём', 'артемий', 'артур', 'аскольд', 'афанасий', 'богдан', 'борис',
        'борислав', 'бронислав', 'вадим', 'валентин', 'валерий', 'варлам', 'василий', 'венедикт', 'вениамин',
        'веньямин', 'венцеслав', 'виктор', 'виген', 'вилен', 'виталий', 'владилен', 'владимир', 'владислав', 'владлен',
        'вова', 'всеволод', 'всеслав', 'вячеслав', 'гавриил', 'геннадий', 'георгий', 'герман', 'глеб', 'григорий',
        'давид', 'даниил', 'данил', 'данила', 'демьян', 'денис', 'димитрий', 'дмитрий', 'добрыня', 'евгений', 'евдоким',
        'евсей', 'егор', 'емельян', 'еремей', 'ермолай', 'ерофей', 'ефим', 'захар', 'иван', 'игнат', 'игорь',
        'илларион', 'иларион', 'илья', 'иосиф', 'казимир', 'касьян', 'кирилл', 'кондрат', 'константин', 'кузьма',
        'лавр', 'лаврентий', 'лазарь', 'ларион', 'лев', 'леонард', 'леонид', 'лука', 'максим', 'марат', 'мартын',
        'матвей', 'мефодий', 'мирон', 'михаил', 'моисей', 'назар', 'никита', 'николай', 'олег', 'осип', 'остап',
        'павел', 'панкрат', 'пантелей', 'парамон', 'пётр', 'петр', 'платон', 'потап', 'прохор', 'роберт', 'ростислав',
        'савва', 'савелий', 'семён', 'семен', 'сергей', 'сидор', 'спартак', 'тарас', 'терентий', 'тимофей', 'тимур',
        'тихон', 'ульян', 'фёдор', 'федор', 'федот', 'феликс', 'фирс', 'фома', 'харитон', 'харлам', 'эдуард',
        'эммануил', 'эраст', 'юлиан', 'юлий', 'юрий', 'яков', 'ян', 'ярослав',
    ];

    protected static $womenNames = [
        'авдотья', 'аврора', 'агата', 'агния', 'агриппина', 'ада', 'аксинья', 'алевтина', 'александра', 'алёна',
        'алена', 'алина', 'алиса', 'алла', 'альбина', 'амалия', 'анастасия', 'ангелина', 'анжела', 'анжелика', 'анна',
        'антонина', 'анфиса', 'арина', 'белла', 'божена', 'валентина', 'валерия', 'ванда', 'варвара', 'василина',
        'василиса', 'вера', 'вероника', 'виктория', 'виола', 'виолетта', 'вита', 'виталия', 'владислава', 'власта',
        'галина', 'глафира', 'дарья', 'диана', 'дина', 'ева', 'евгения', 'евдокия', 'евлампия', 'екатерина', 'елена',
        'елизавета', 'ефросиния', 'ефросинья', 'жанна', 'зиновия', 'злата', 'зоя', 'ивонна', 'изольда', 'илона', 'инга',
        'инесса', 'инна', 'ирина', 'ия', 'капитолина', 'карина', 'каролина', 'кира', 'клавдия', 'клара', 'клеопатра',
        'кристина', 'ксения', 'лада', 'лариса', 'лиана', 'лидия', 'лилия', 'лина', 'лия', 'лора', 'любава', 'любовь',
        'людмила', 'майя', 'маргарита', 'марианна', 'мариетта', 'марина', 'мария', 'марья', 'марта', 'марфа', 'марьяна',
        'матрёна', 'матрена', 'матрона', 'милена', 'милослава', 'мирослава', 'муза', 'надежда', 'настасия', 'настасья',
        'наталия', 'наталья', 'нелли', 'ника', 'нина', 'нинель', 'нонна', 'оксана', 'олимпиада', 'ольга', 'пелагея',
        'полина', 'прасковья', 'раиса', 'рената', 'римма', 'роза', 'роксана', 'руфь', 'сарра', 'светлана', 'серафима',
        'снежана', 'софья', 'софия', 'стелла', 'степанида', 'стефания', 'таисия', 'таисья', 'тамара', 'татьяна',
        'ульяна', 'устиния', 'устинья', 'фаина', 'фёкла', 'фекла', 'феодора', 'хаврония', 'христина', 'эвелина',
        'эдита', 'элеонора', 'элла', 'эльвира', 'эмилия', 'эмма', 'юдифь', 'юлиана', 'юлия', 'ядвига', 'яна',
        'ярослава',
    ];

    protected static $immutableNames = [
        'николя',
    ];

    /**
     * Checks if name is mutable
     * @param string $name
     * @param null|string $gender
     * @return bool
     */
    public static function isMutable($name, $gender = null)
    {
        $name = S::lower($name);

        if (in_array($name, static::$immutableNames, true)) {
            return false;
        }

        if ($gender === null) {
            $gender = static::detectGender($name);
        }

        // man rules
        if ($gender === static::MALE) {
            // soft consonant
            if (S::lower(S::slice($name, -1)) == 'ь' && static::isConsonant(S::slice($name, -2, -1))) {
                return true;
            } elseif (in_array(S::slice($name, -1), array_diff(static::$consonants, ['й', /*'Ч', 'Щ'*/]), true)) { // hard consonant
                return true;
            } elseif (S::slice($name, -1) == 'й') {
                return true;
            } else if (in_array(S::slice($name, -2), ['ло', 'ко'], true)) {
                return true;
            }
        } else if ($gender === static::FEMALE) {
            // soft consonant
            if (S::lower(S::slice($name, -1)) == 'ь' && static::isConsonant(S::slice($name, -2, -1))) {
                return true;
            } else if (static::isHissingConsonant(S::slice($name, -1))) {
                return true;
            }
        }

        // common rules
        if ((in_array(S::slice($name, -1), ['а', 'я']) && !static::isVowel(S::slice($name, -2, -1))) || in_array(S::slice($name, -2), ['ия', 'ья', 'ея', 'оя'], true)) {
            return true;
        }

        return false;
    }

    /**
     * @param $name
     * @return string
     */
    public static function detectGender($name)
    {
        $name = S::lower($name);
        if (in_array($name, static::$menNames, true)) {
            return static::MALE;
        } elseif (in_array($name, static::$womenNames, true)) {
            return static::FEMALE;
        }

        $man = $woman = 0;
        $last1 = S::slice($name, -1);
        $last2 = S::slice($name, -2);
        $last3 = S::slice($name, -3);

        // try to detect gender by some statistical rules
        //
        if ($last1 == 'й') {
            $man += 0.9;
        }
        if ($last1 == 'ь') {
            $man += 0.02;
        }
        if (in_array($last1, static::$consonants, true)) {
            $man += 0.01;
        }
        if (in_array($last2, ['он', 'ов', 'ав', 'ам', 'ол', 'ан', 'рд', 'мп'], true)) {
            $man += 0.3;
        }
        if (in_array($last2, ['вь', 'фь', 'ль'], true)) {
            $woman += 0.1;
        }
        if (in_array($last2, ['ла'], true)) {
            $woman += 0.04;
        }
        if (in_array($last2, ['то', 'ма'], true)) {
            $man += 0.01;
        }
        if (in_array($last3, ['лья', 'вва', 'ока', 'ука', 'ита'], true)) {
            $man += 0.2;
        }
        if (in_array($last3, ['има'], true)) {
            $woman += 0.15;
        }
        if (in_array($last3, ['лия', 'ния', 'сия', 'дра', 'лла', 'кла', 'опа'], true)) {
            $woman += 0.5;
        }
        if (in_array(S::slice($name, -4), ['льда', 'фира', 'нина', 'лита', 'алья'], true)) {
            $woman += 0.5;
        }

        return $man === $woman ? null
            : ($man > $woman ? static::MALE : static::FEMALE);
    }

    /**
     * @param string $name
     * @param null|string $gender
     * @return array
     */
    public static function getCases($name, $gender = null)
    {
        $name = S::lower($name);

        if (static::isMutable($name, $gender)) {
            // common rules for ия and я
            if (S::slice($name, -2) == 'ия') {
                $prefix = S::name(S::slice($name, 0, -1));
                return [
                    static::IMENIT => $prefix.'я',
                    static::RODIT => $prefix.'и',
                    static::DAT => $prefix.'и',
                    static::VINIT => $prefix.'ю',
                    static::TVORIT => $prefix.'ей',
                    static::PREDLOJ => $prefix.'и',
                ];
            } elseif (S::slice($name, -1) == 'я') {
                $prefix = S::name(S::slice($name, 0, -1));
                return [
                    static::IMENIT => $prefix.'я',
                    static::RODIT => $prefix.'и',
                    static::DAT => $prefix.'е',
                    static::VINIT => $prefix.'ю',
                    static::TVORIT => $prefix.'ей',
                    static::PREDLOJ => $prefix.'е',
                ];
            }

            if (!in_array($name, static::$immutableNames, true)) {
                if ($gender === null) {
                    $gender = static::detectGender($name);
                }
                if ($gender === static::MALE || $name === 'саша') {
                    if (($result = static::getCasesMan($name)) !== null) {
                        return $result;
                    }
                } elseif ($gender === static::FEMALE) {
                    if (($result = static::getCasesWoman($name)) !== null) {
                        return $result;
                    }
                }
            }
        }

        $name = S::name($name);
        return array_fill_keys(array(static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ), $name);
    }

    /**
     * @param string $name
     * @return array|null
     */
    protected static function getCasesMan($name)
    {
        // special cases for Лев, Павел
        if (isset(static::$exceptions[$name])) {
            return static::$exceptions[$name];
        } elseif (in_array(S::slice($name, -1), array_diff(static::$consonants, ['й', /*'Ч', 'Щ'*/]), true)) { // hard consonant
			if (in_array(S::slice($name, -2), ['ек', 'ёк'], true)) { // Витек, Санек
                // case for foreign names like Салмонбек
                if (static::isConsonant(S::slice($name, -4, -3)))
                    $prefix = S::name(S::slice($name, 0, -2)).'ек';
                else
				    $prefix = S::name(S::slice($name, 0, -2)).'ьк';
			} else {
                if ($name === 'пётр')
                    $prefix = S::name(str_replace('ё', 'е', $name));
                else
				    $prefix = S::name($name);
            }
            return [
                static::IMENIT => S::name($name),
                static::RODIT => $prefix.'а',
                static::DAT => $prefix.'у',
                static::VINIT => $prefix.'а',
                static::TVORIT => RussianLanguage::isHissingConsonant(S::slice($name, -1)) || S::slice($name, -1) == 'ц' ? $prefix.'ем' : $prefix.'ом',
                static::PREDLOJ => $prefix.'е',
            ];
        } elseif (S::slice($name, -1) == 'ь' && static::isConsonant(S::slice($name, -2, -1))) { // soft consonant
            $prefix = S::name(S::slice($name, 0, -1));
            return [
                static::IMENIT => $prefix.'ь',
                static::RODIT => $prefix.'я',
                static::DAT => $prefix.'ю',
                static::VINIT => $prefix.'я',
                static::TVORIT => $prefix.'ем',
                static::PREDLOJ => $prefix.'е',
            ];
        } elseif (in_array(S::slice($name, -2), ['ай', 'ей', 'ой', 'уй', 'яй', 'юй', 'ий'], true)) {
            $prefix = S::name(S::slice($name, 0, -1));
            $postfix = S::slice($name, -2) == 'ий' ? 'и' : 'е';
            return [
                static::IMENIT => $prefix.'й',
                static::RODIT => $prefix.'я',
                static::DAT => $prefix.'ю',
                static::VINIT => $prefix.'я',
                static::TVORIT => $prefix.'ем',
                static::PREDLOJ => $prefix.$postfix,
            ];
        } elseif (S::slice($name, -1) == 'а' && static::isConsonant($before = S::slice($name, -2, -1)) && !in_array($before, [/*'г', 'к', 'х', */'ц'], true)) {
            $prefix = S::name(S::slice($name, 0, -1));
            $postfix = (RussianLanguage::isHissingConsonant($before) || in_array($before, ['г', 'к', 'х'], true)) ? 'и' : 'ы';
            return [
                static::IMENIT => $prefix.'а',
                static::RODIT => $prefix.$postfix,
                static::DAT => $prefix.'е',
                static::VINIT => $prefix.'у',
                static::TVORIT => $prefix.($before === 'ш' ? 'е' : 'о').'й',
                static::PREDLOJ => $prefix.'е',
            ];
        } elseif (S::slice($name, -2) == 'ло' || S::slice($name, -2) == 'ко') {
            $prefix = S::name(S::slice($name, 0, -1));
            $postfix = S::slice($name, -2, -1) == 'к' ? 'и' : 'ы';
            return [
                static::IMENIT => $prefix.'о',
                static::RODIT =>  $prefix.$postfix,
                static::DAT => $prefix.'е',
                static::VINIT => $prefix.'у',
                static::TVORIT => $prefix.'ой',
                static::PREDLOJ => $prefix.'е',
            ];
        }

        return null;
    }

    /**
     * @param string $name
     * @return array|null
     */
    protected static function getCasesWoman($name)
    {
        if (S::slice($name, -1) == 'а' && !static::isVowel($before = (S::slice($name, -2, -1)))) {
            $prefix = S::name(S::slice($name, 0, -1));
            if ($before != 'ц') {
                $postfix = (RussianLanguage::isHissingConsonant($before) || in_array($before, ['г', 'к', 'х'], true)) ? 'и' : 'ы';
                return [
                    static::IMENIT => $prefix.'а',
                    static::RODIT => $prefix.$postfix,
                    static::DAT => $prefix.'е',
                    static::VINIT => $prefix.'у',
                    static::TVORIT => $prefix.'ой',
                    static::PREDLOJ => $prefix.'е',
                ];
            } else {
                return [
                    static::IMENIT => $prefix.'а',
                    static::RODIT => $prefix.'ы',
                    static::DAT => $prefix.'е',
                    static::VINIT => $prefix.'у',
                    static::TVORIT => $prefix.'ей',
                    static::PREDLOJ => $prefix.'е',
                ];
            }
        } elseif (S::slice($name, -1) == 'ь' && static::isConsonant(S::slice($name, -2, -1))) {
            $prefix = S::name(S::slice($name, 0, -1));
            return [
                static::IMENIT => $prefix.'ь',
                static::RODIT => $prefix.'и',
                static::DAT => $prefix.'и',
                static::VINIT => $prefix.'ь',
                static::TVORIT => $prefix.'ью',
                static::PREDLOJ => $prefix.'и',
            ];
        } elseif (RussianLanguage::isHissingConsonant(S::slice($name, -1))) {
            $prefix = S::name($name);
            return [
                static::IMENIT => $prefix,
                static::RODIT => $prefix.'и',
                static::DAT => $prefix.'и',
                static::VINIT => $prefix,
                static::TVORIT => $prefix.'ью',
                static::PREDLOJ => $prefix.'и',
            ];
        }
        return null;
    }

    /**
     * @param string $name
     * @param string $case
     * @param null|string $gender
     * @return string
     * @throws \Exception
     */
    public static function getCase($name, $case, $gender = null)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($name, $gender);
        return $forms[$case];
    }
}


<?php
namespace morphos\Russian;

use morphos\NumeralGenerator;
use morphos\S;

/**
 * Rules are from http://www.fio.ru/pravila/grammatika/sklonenie-imen-chislitelnykh/
 */
class CardinalNumeralGenerator extends NumeralGenerator implements Cases
{
    use RussianLanguage, CasesHelper;

    protected static $words = [
        1 => 'один',
        2 => 'два',
        3 => 'три',
        4 => 'четыре',
        5 => 'пять',
        6 => 'шесть',
        7 => 'семь',
        8 => 'восемь',
        9 => 'девять',
        10 => 'десять',
        11 => 'одиннадцать',
        12 => 'двенадцать',
        13 => 'тринадцать',
        14 => 'четырнадцать',
        15 => 'пятнадцать',
        16 => 'шестнадцать',
        17 => 'семнадцать',
        18 => 'восемнадцать',
        19 => 'девятнадцать',
        20 => 'двадцать',
        30 => 'тридцать',
        40 => 'сорок',
        50 => 'пятьдесят',
        60 => 'шестьдесят',
        70 => 'семьдесят',
        80 => 'восемьдесят',
        90 => 'девяносто',
        100 => 'сто',
        200 => 'двести',
        300 => 'триста',
        400 => 'четыреста',
        500 => 'пятьсот',
        600 => 'шестьсот',
        700 => 'семьсот',
        800 => 'восемьсот',
        900 => 'девятьсот',
    ];

    protected static $exponents = [
        '1000' => 'тысяча',
        '1000000' => 'миллион',
        '1000000000' => 'миллиард',
        '1000000000000' => 'триллион',
        '1000000000000000' => 'квадриллион',
    ];

    protected static $precalculated = [
        'один' => [
            self::MALE => [
                self::IMENIT => 'один',
                self::RODIT => 'одного',
                self::DAT => 'одному',
                self::VINIT => 'один',
                self::TVORIT => 'одним',
                self::PREDLOJ => 'одном',
            ],
            self::FEMALE => [
                self::IMENIT => 'одна',
                self::RODIT => 'одной',
                self::DAT => 'одной',
                self::VINIT => 'одну',
                self::TVORIT => 'одной',
                self::PREDLOJ => 'одной',
            ],
            self::NEUTER => [
                self::IMENIT => 'одно',
                self::RODIT => 'одного',
                self::DAT => 'одному',
                self::VINIT => 'одно',
                self::TVORIT => 'одним',
                self::PREDLOJ => 'одном',
            ],
        ],
        'два' => [
            self::MALE => [
                self::IMENIT => 'два',
                self::RODIT => 'двух',
                self::DAT => 'двум',
                self::VINIT => 'два',
                self::TVORIT => 'двумя',
                self::PREDLOJ => 'двух',
            ],
            self::FEMALE => [
                self::IMENIT => 'две',
                self::RODIT => 'двух',
                self::DAT => 'двум',
                self::VINIT => 'две',
                self::TVORIT => 'двумя',
                self::PREDLOJ => 'двух',
            ],
            self::NEUTER => [
                self::IMENIT => 'два',
                self::RODIT => 'двух',
                self::DAT => 'двум',
                self::VINIT => 'два',
                self::TVORIT => 'двумя',
                self::PREDLOJ => 'двух',
            ],
        ],
        'три' => [
            self::IMENIT => 'три',
            self::RODIT => 'трех',
            self::DAT => 'трем',
            self::VINIT => 'три',
            self::TVORIT => 'тремя',
            self::PREDLOJ => 'трех',
        ],
        'четыре' => [
            self::IMENIT => 'четыре',
            self::RODIT => 'четырех',
            self::DAT => 'четырем',
            self::VINIT => 'четыре',
            self::TVORIT => 'четырьмя',
            self::PREDLOJ => 'четырех',
        ],
        'двести' => [
            self::IMENIT => 'двести',
            self::RODIT => 'двухсот',
            self::DAT => 'двумстам',
            self::VINIT => 'двести',
            self::TVORIT => 'двумястами',
            self::PREDLOJ => 'двухстах',
        ],
        'восемьсот' => [
            self::IMENIT => 'восемьсот',
            self::RODIT => 'восьмисот',
            self::DAT => 'восьмистам',
            self::VINIT => 'восемьсот',
            self::TVORIT => 'восьмистами',
            self::PREDLOJ => 'восьмистах',
        ],
        'тысяча' => [
            self::IMENIT => 'тысяча',
            self::RODIT => 'тысяч',
            self::DAT => 'тысячам',
            self::VINIT => 'тысяч',
            self::TVORIT => 'тысячей',
            self::PREDLOJ => 'тысячах',
        ],
    ];

    /**
     * @param $number
     * @param string $gender
     * @return array
     * @throws \Exception
     */
    public static function getCases($number, $gender = self::MALE)
    {
        // simple numeral
        if (isset(static::$words[$number]) || isset(static::$exponents[$number])) {
            $word = isset(static::$words[$number]) ? static::$words[$number] : static::$exponents[$number];
            if (isset(static::$precalculated[$word])) {
                if (isset(static::$precalculated[$word][static::MALE])) {
                    return static::$precalculated[$word][$gender];
                } else {
                    return static::$precalculated[$word];
                }
            } elseif (($number >= 5 && $number <= 20) || $number == 30) {
                $prefix = S::slice($word, 0, -1);
                return [
                    static::IMENIT => $word,
                    static::RODIT => $prefix.'и',
                    static::DAT => $prefix.'и',
                    static::VINIT => $word,
                    static::TVORIT => $prefix.'ью',
                    static::PREDLOJ => $prefix.'и',
                ];
            } elseif (in_array($number, [40, 90, 100])) {
                $prefix = $number == 40 ? $word : S::slice($word, 0, -1);
                return [
                    static::IMENIT => $word,
                    static::RODIT => $prefix.'а',
                    static::DAT => $prefix.'а',
                    static::VINIT => $word,
                    static::TVORIT => $prefix.'а',
                    static::PREDLOJ => $prefix.'а',
                ];
            } elseif (($number >= 50 && $number <= 80)) {
                $prefix = S::slice($word, 0, -6);
                return [
                    static::IMENIT => $prefix.'ьдесят',
                    static::RODIT => $prefix.'идесяти',
                    static::DAT => $prefix.'идесяти',
                    static::VINIT => $prefix.'ьдесят',
                    static::TVORIT => $prefix.'ьюдесятью',
                    static::PREDLOJ => $prefix.'идесяти',
                ];
            } elseif (in_array($number, [300, 400])) {
                $prefix = S::slice($word, 0, -4);
                return [
                    static::IMENIT => $word,
                    static::RODIT => $prefix.'ехсот',
                    static::DAT => $prefix.'емстам',
                    static::VINIT => $word,
                    static::TVORIT => $prefix.($number == 300 ? 'е' : 'ь').'мястами',
                    static::PREDLOJ => $prefix.'ехстах',
                ];
            } elseif ($number >= 500 && $number <= 900) {
                $prefix = S::slice($word, 0, -4);
                return [
                    static::IMENIT => $word,
                    static::RODIT => $prefix.'исот',
                    static::DAT => $prefix.'истам',
                    static::VINIT => $word,
                    static::TVORIT => $prefix.'ьюстами',
                    static::PREDLOJ => $prefix.'истах',
                ];
            } elseif (isset(static::$exponents[$number])) {
                return NounDeclension::getCases($word, false);
            }
        } elseif ($number == 0) {
            return [
                static::IMENIT => 'ноль',
                static::RODIT => 'ноля',
                static::DAT => 'нолю',
                static::VINIT => 'ноль',
                static::TVORIT => 'нолём',
                static::PREDLOJ => 'ноле',
            ];
        }
        // compound numeral
        else {
            $parts = [];
            $result = [];

            foreach (array_reverse(static::$exponents, true) as $word_number => $word) {
                if ($number >= $word_number) {
                    $count = floor($number / $word_number);
                    $parts[] = static::getCases($count, ($word_number == 1000 ? static::FEMALE : static::MALE));

                    switch (NounPluralization::getNumeralForm($count)) {
                        case NounPluralization::ONE:
                            $parts[] = NounDeclension::getCases($word, false);
                            break;

                        case NounPluralization::TWO_FOUR:
                            $part = NounPluralization::getCases($word);
                            if ($word_number != 1000) { // get dative case of word for 1000000, 1000000000 and 1000000000000
                                $part[Cases::IMENIT] = $part[Cases::VINIT] = NounDeclension::getCase($word, Cases::RODIT);
                            }
                            $parts[] = $part;
                            break;

                        case NounPluralization::FIVE_OTHER:
                            $part = NounPluralization::getCases($word);
                            $part[Cases::IMENIT] = $part[Cases::VINIT] = $part[Cases::RODIT];
                            $parts[] = $part;
                            break;
                    }

                    $number = $number % ($count * $word_number);
                }
            }

            foreach (array_reverse(static::$words, true) as $word_number => $word) {
                if ($number >= $word_number) {
                    $parts[] = static::getCases($word_number, $gender);
                    $number %= $word_number;
                }
            }

            // make one array with cases and delete 'o/об' prepositional from all parts except the last one
            foreach (array(static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ) as $case) {
                $result[$case] = [];
                foreach ($parts as $partN => $part) {
                    $result[$case][] = $part[$case];
                }
                $result[$case] = implode(' ', $result[$case]);
            }

            return $result;
        }
    }

    /**
     * @param        $number
     * @param        $case
     * @param string $gender
     *
     * @return mixed|void
     * @throws \Exception
     */
    public static function getCase($number, $case, $gender = self::MALE)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($number, $gender);
        return $forms[$case];
    }
}