diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..58ecaab --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "mirzaev/minimal-sessions-arangodb", + "description": "Module for session system based on the minimal framework using the ArangoDB database", + "keywords": [ + "minimal", + "sessions", + "arangodb" + ], + "type": "minimal-module", + "license": "WTFPL", + "homepage": "https://git.mirzaev.sexy/mirzaev/minimal-sessions-arangodb", + "authors": [ + { + "name": "Arsen Mirzaev Tatyano-Muradovich", + "email": "arsen@mirzaev.sexy", + "homepage": "https://mirzaev.sexy", + "role": "Programmer" + } + ], + "require": { + "php": "^8.1", + "mirzaev/minimal": "^2.0" + }, + "autoload": { + "psr-4": { + "mirzaev\\minimal\\sessions\\arangodb\\": "mirzaev/minimal-sessions-arangodb/system" + } + }, + "autoload-dev": { + "psr-4": { + "mirzaev\\minimal\\sessions\\arangodb\\tests\\": "mirzaev/minimal-sessions-arangodb/tests" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..634f24a --- /dev/null +++ b/composer.lock @@ -0,0 +1,65 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d14d4eec14bd24ff19dbca47317e07ac", + "packages": [ + { + "name": "mirzaev/minimal", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://git.mirzaev.sexy/mirzaev/minimal", + "reference": "22ad7304f980dc097cbb29d1c9aa130b59a7a58f" + }, + "require": { + "php": "~8.1" + }, + "suggest": { + "ext-PDO": "To work with SQL-based databases (MySQL, PostreSQL...)" + }, + "type": "framework", + "autoload": { + "psr-4": { + "mirzaev\\minimal\\": "mirzaev/minimal/system" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Arsen Mirzaev Tatyano-Muradovich", + "email": "arsen@mirzaev.sexy", + "homepage": "https://mirzaev.sexy", + "role": "Developer" + } + ], + "description": "Lightweight MVC framework that manages only the basic mechanisms, leaving the development of the programmer and not overloading the project", + "homepage": "https://git.mirzaev.sexy/mirzaev/minimal", + "keywords": [ + "framework", + "mvc" + ], + "support": { + "docs": "https://git.mirzaev.sexy/mirzaev/minimal/wiki", + "issues": "https://git.mirzaev.sexy/mirzaev/minimal/issues" + }, + "time": "2022-11-02T22:27:45+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/mirzaev/minimal-sessions-arangodb/system/models/core.php b/mirzaev/minimal-sessions-arangodb/system/models/core.php new file mode 100644 index 0000000..7c8de46 --- /dev/null +++ b/mirzaev/minimal-sessions-arangodb/system/models/core.php @@ -0,0 +1,146 @@ + + */ +class core extends model +{ + /** + * Коллекция в которой хранятся аккаунты + */ + public const SETTINGS = '../settings/arangodb.php'; + + /** + * Соединение с базой данных + */ + protected static connection $db; + + public function __construct(connection $db = null) + { + if (isset($db)) { + // Получена инстанция соединения с базой данных + + // Запись и инициализация соединения с базой данных + $this->__set('db', $db); + } else { + // Не получена инстанция соединения с базой данных + + // Инициализация соединения с базой данных по умолчанию + $this->__get('db'); + } + } + + /** + * Записать свойство + * + * @param string $name Название + * @param mixed $value Значение + */ + public function __set(string $name, mixed $value = null): void + { + match ($name) { + 'db' => (function () use ($value) { + if ($this->__isset('db')) { + // Свойство уже было инициализировано + + // Выброс исключения (неудача) + throw new exception('Запрещено реинициализировать соединение с базой данных ($this->db)', 500); + } else { + // Свойство ещё не было инициализировано + + if ($value instanceof connection) { + // Передано подходящее значение + + // Запись свойства (успех) + self::$db = $value; + } else { + // Передано неподходящее значение + + // Выброс исключения (неудача) + throw new exception('Соединение с базой данных ($this->db) должен быть инстанцией mirzaev\arangodb\connection', 500); + } + } + })(), + default => parent::__set($name, $value) + }; + } + + /** + * Прочитать свойство + * + * @param string $name Название + * + * @return mixed Содержимое + */ + public function __get(string $name): mixed + { + return match ($name) { + 'db' => (function () { + if (!$this->__isset('db')) { + // Свойство не инициализировано + + // Инициализация значения по умолчанию исходя из настроек + $this->__set('db', new connection(require static::SETTINGS)); + } + + return self::$db; + })(), + default => parent::__get($name) + }; + } + + /** + * Проверить свойство на инициализированность + * + * @param string $name Название + */ + public function __isset(string $name): bool + { + return match ($name) { + default => parent::__isset($name) + }; + } + + /** + * Удалить свойство + * + * @param string $name Название + */ + public function __unset(string $name): void + { + match ($name) { + default => parent::__isset($name) + }; + } + + + /** + * Статический вызов + * + * @param string $name Название + * @param array $arguments Параметры + */ + public static function __callStatic(string $name, array $arguments): mixed + { + match ($name) { + 'db' => (new static)->__get('db'), + default => throw new exception("Не найдено свойство или функция: $name", 500) + }; + } +} diff --git a/mirzaev/minimal-sessions-arangodb/system/models/session_model.php b/mirzaev/minimal-sessions-arangodb/system/models/session_model.php new file mode 100644 index 0000000..4e1d9bc --- /dev/null +++ b/mirzaev/minimal-sessions-arangodb/system/models/session_model.php @@ -0,0 +1,213 @@ + + */ +final class session_model extends core +{ + /** + * Коллекция + */ + public const COLLECTION = 'session'; + + /** + * Инициализация + * + * @param ?string $hash Хеш сессии в базе данных + * @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии) + * @param array &$errors Журнал ошибок + * + * @return ?_document Инстанция сессии, если удалось найти или создать + */ + public static function initialization(?string $hash = null, ?int $expires = null, array &$errors = []): ?_document + { + try { + if (collection::init(static::$db->session, self::COLLECTION)) { + // Инициализирована коллекция + + if (isset($hash) && $session = collection::search(static::$db->session, sprintf( + << %d + RETURN d + AQL, + self::COLLECTION, + time() + ))) { + // Найдена сессия по хешу + + // Возврат сессии + return $session; + } else if ($session = collection::search(static::$db->session, sprintf( + << %d + RETURN d + AQL, + self::COLLECTION, + $_SERVER['REMOTE_ADDR'], + time() + ))) { + // Найдена сессия по данным пользователя + + // Возврат сессии + return $session; + } else { + // Не найдена сессия + + // Запись сессии в базу данных + $_id = document::write(static::$db->session, self::COLLECTION, [ + 'ip' => $_SERVER['REMOTE_ADDR'], + 'expires' => $expires ?? time() + 604800 + ]); + + if ($session = collection::search(static::$db->session, sprintf( + << %d + RETURN d + AQL, + self::COLLECTION, + time() + ))) { + // Найдена созданная сессия + + // Запись хеша + $session->hash = sodium_bin2hex(sodium_crypto_generichash($_id)); + + if (document::update(static::$db->session, $session)) { + // Записано обновление + + return $session; + } else throw new exception('Не удалось записать данные сессии'); + } else throw new exception('Не удалось создать или найти созданную сессию'); + } + } else throw new exception('Не удалось инициализировать коллекцию'); + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return null; + } + + /** + * Связь сессии с аккаунтом + * + * @param _document $session Инстанция сессии + * @param _document $account Инстанция аккаунта + * @param array &$errors Журнал ошибок + * + * @return bool Статус выполнения + */ + public static function connect(_document $session, _document $account, array &$errors = []): bool + { + try { + if ( + collection::init(static::$db->session, self::COLLECTION) + && collection::init(static::$db->session, account::COLLECTION) + && collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true) + ) { + // Инициализирована коллекция + + if (document::write(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, [ + '_from' => $session->getId(), + '_to' => $account->getId() + ])) { + // Создано ребро: session -> account + + return true; + } else throw new exception('Не удалось создать ребро: session -> account'); + } else throw new exception('Не удалось инициализировать коллекцию'); + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return false; + } + + /** + * Поиск связанного аккаунта + * + * @param _document $session Инстанция сессии + * @param array &$errors Журнал ошибок + * + * @return ?_document Инстанция аккаунта, если удалось найти + */ + public static function account(_document $session, array &$errors = []): ?_document + { + try { + if ( + collection::init(static::$db->session, self::COLLECTION) + && collection::init(static::$db->session, account::COLLECTION) + && collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true) + ) { + // Инициализированы коллекции + + if ($account = collection::search(static::$db->session, sprintf( + <<getId() + ))) { + // Найден аккаунт + + return $account; + } else throw new exception('Не удалось найти аккаунт'); + } else throw new exception('Не удалось инициализировать коллекцию'); + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return null; + } +}