commit ab752bf34f415f4ef79f3105f2e187c9b93b9f72 Author: Arsen Mirzaev Tatyano-Muradovich Date: Sun Mar 6 05:21:24 2022 +1000 Инициализация diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49ce3c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0a56bd8 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "mirzaev/surikovlib", + "description": "Онлайн библеотека музея имени Сурикова", + "type": "project", + "license": "AGPL-3.0-or-later", + "homepage": "https://git.hood.su/mirzaev/surikovlib", + "authors": [ + { + "name": "Arsen Mirzaev Tatyano-Muradovich", + "email": "arsen@mirzaev.sexy", + "homepage": "https://mirzaev.sexy", + "role": "Developer" + } + ], + "require": { + "php": "^8.0.0", + "mirzaev/minimal": "^2.0.x-dev", + "twig/twig": "^3.3" + }, + "autoload": { + "psr-4": { + "mirzaev\\surikovlib\\": "mirzaev/surikovlib/system" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4d7856d --- /dev/null +++ b/composer.lock @@ -0,0 +1,302 @@ +{ + "_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": "1c826a114d11e8301a0f17171d987459", + "packages": [ + { + "name": "mirzaev/minimal", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://git.hood.su/mirzaev/minimal", + "reference": "b6f90b700116f20fe48725166ddfb8f6d27ae52d" + }, + "require": { + "php": "~8.0" + }, + "suggest": { + "ext-PDO": "Для работы с базами данных на SQL (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": "Легковесный MVC фреймворк который следует твоим правилам, а не диктует свои", + "homepage": "https://git.hood.su/mirzaev/minimal", + "keywords": [ + "framework", + "mvc" + ], + "support": { + "docs": "https://git.hood.su/mirzaev/minimal/manual", + "issues": "https://git.hood.su/mirzaev/minimal/issues" + }, + "time": "2021-11-12T13:20:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.23.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T12:26:48+00:00" + }, + { + "name": "twig/twig", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a27fa056df8a6384316288ca8b0fa3a35fdeb569" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a27fa056df8a6384316288ca8b0fa3a35fdeb569", + "reference": "a27fa056df8a6384316288ca8b0fa3a35fdeb569", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.3.3" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2021-09-17T08:44:23+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "mirzaev/minimal": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.0.0" + }, + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/mirzaev/surikovlib/system/controllers/accounts_controller.php b/mirzaev/surikovlib/system/controllers/accounts_controller.php new file mode 100644 index 0000000..f3b250c --- /dev/null +++ b/mirzaev/surikovlib/system/controllers/accounts_controller.php @@ -0,0 +1,168 @@ + + */ +final class accounts_controller extends core +{ + /** + * Страница профиля + * + * @param array $vars + */ + public function index(array $vars = []): ?string + { + return null; + } + + /** + * Регистрация + * + * @param array $vars Параметры запроса + * + * @return string|null HTML-документ + */ + public function registration(array $vars = []): ?string + { + // Инициализация журнала ошибок + $vars['errors'] = ['account' => []]; + + if ($vars['account'] = accounts::registration(email: $vars['email'] ?? null, password: $vars['password'] ?? null, errors: $vars['errors']['account'])) { + // Удалось зарегистрироваться + + if ($vars['account'] = accounts::authentication($vars['email'] ?? null, $vars['password'] ?? null, (bool) ($vars['remember'] ?? false), $vars)) { + // Удалось аутентифицироваться + } else { + // Не удалось аутентифицироваться + + // Запись кода ответа + http_response_code(401); + } + } else { + // Не удалось зарегистрироваться + + // Запись кода ответа + http_response_code(401); + } + + // Инициализаци пути для перенаправления + $redirect = isset($vars['redirect']) ? $vars['redirect'] : $_SERVER['HTTP_REFERER'] ?? '/'; + + // Перенаправление + header("Location: $redirect", response_code: 303); + + return null; + } + + /** + * Аутентификация + * + * @param array $vars Параметры запроса + * + * @return string|null HTML-документ + */ + public function authentication(array $vars = []): ?string + { + // Инициализация журнала ошибок + $vars['errors'] = ['account' => []]; + + if ($vars['account'] = accounts::authentication($vars['email'] ?? null, $vars['password'] ?? null, (bool) ($vars['remember'] ?? false), errors: $vars['errors']['account'])) { + // Удалось аутентифицироваться + } else { + // Не удалось аутентифицироваться + + // Запись кода ответа + http_response_code(401); + } + + // Инициализаци пути для перенаправления + $redirect = isset($vars['redirect']) ? $vars['redirect'] : $_SERVER['HTTP_REFERER'] ?? '/'; + + // Перенаправление + header("Location: $redirect", response_code: 303); + + return null; + } + + /** + * Деаутентификация + * + * @param array $vars Параметры запроса + * + * @return string|null HTML-документ + */ + public function deauthentication(array $vars = []): ?string + { + // Инициализация журнала ошибок + $vars['errors'] = ['account' => []]; + + if (accounts::deauthentication(errors: $vars['errors']['account'])) { + // Удалось деаутентифицироваться + + // Деинициализация аккаунта + $vars['account'] = null; + } else { + // Не удалось деаутентифицироваться + + // Запись кода ответа + http_response_code(500); + } + + // Перенаправление + header('Location: /', response_code: 303); + + return null; + } + + /** + * Данные аккаунта + * + * Если информацию запрашивает администратор, то вернётся вся, иначе только разрешённая публично + * + * @param array $vars Параметры запроса + * + * @return string JSON-документ + */ + public function data(array $vars = []): ?string + { + // Инициализация журнала ошибок + $vars['errors'] = ['account' => []]; + + if ($account = accounts::read(['id' => $vars['id']], $vars['errors'])) { + // Найдены данные запрашиваемого аккаунта + + // Инициализация аккаунта + $vars['account'] = accounts::account($vars['errors']); + + if ($vars['account'] && $vars['account']['permissions']['accounts'] ?? 0 === 1) { + // Удалось аутентифицироваться и пройдена проверка авторизации + } else { + // Не удалось аутентифицироваться + + // Удаление запрещённых к публикации полей + $account['password'] = $account['hash'] = $account['time'] = null; + } + + // Генерация ответа + return json_encode($account ?? ''); + } else { + // Не найдены данные запрашиваемого аккаунта + + // Запись кода ответа + http_response_code(404); + } + + return null; + } +} diff --git a/mirzaev/surikovlib/system/controllers/books_controller.php b/mirzaev/surikovlib/system/controllers/books_controller.php new file mode 100644 index 0000000..eeaafb5 --- /dev/null +++ b/mirzaev/surikovlib/system/controllers/books_controller.php @@ -0,0 +1,170 @@ + + */ +final class books_controller extends core +{ + /** + * Страница с библеотекой + * + * @param array $vars + */ + public function index(array $vars = []): ?string + { + // Проверка аутентифицированности + $vars['account'] = accounts::account($vars); + + if (!empty($vars['id'])) { + // Определённая книга + + // Чтение книги + $vars['book'] = books::read(['id' => $vars['id']])[0] ?? null; + + if (empty($vars['book'])) { + // Не найдена книга + + return null; + } + + // Генерация представления + return $this->view->render(DIRECTORY_SEPARATOR . 'books' . DIRECTORY_SEPARATOR . 'book.html', $vars); + } else { + // Все книги + + // Чтение книг + $vars['books'] = books::read(limit: 30, page: isset($vars['page']) && $vars['page'] > 0 ? $vars['page'] : 1); + + // Генерация представления + return $this->view->render(DIRECTORY_SEPARATOR . 'books' . DIRECTORY_SEPARATOR . 'index.html', $vars); + } + } + + /** + * Обложка + * + * @param array $vars + * + * @return string|null Файл, если найден + */ + public function cover(array $vars = []): ?string + { + // Проверка входных параметров + if (empty($vars['id'])) return null; + + // Инициализация пути до файла + $file = \STORAGE . DIRECTORY_SEPARATOR . 'books' . DIRECTORY_SEPARATOR . $vars['id'] . DIRECTORY_SEPARATOR . '0.jpg'; + + if (file_exists($file)) { + // Найден файл + + // Настройка заголовков + header('Content-Description: File Transfer'); + header('Content-Type: image/jpeg'); + header('Content-Disposition: attachment; filename=' . basename($file)); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: ' . filesize($file)); + + // Очистить буфер вывода + ob_end_clean(); + + return file_get_contents($file); + } + + return null; + } + + /** + * Запись + * + * @param array $vars Параметры + * @param array $files Файлы + * + * @return string|null HTML-документ + */ + public function write(array $vars = [], array $files = []): ?string + { + // ПЕРЕНЕСТИ В МОДЕЛЬ + + // Инициализация буфера сохранённых книг + $books = []; + + for ($i = -1; count($files['books']['name']) > ++$i;) { + // Перебор загруженных книг + + // Генерация хеша файла + $hash = hash_file('md5', $files['books']['tmp_name'][$i]) ?? 0; + + if (move_uploaded_file($files['books']['tmp_name'][$i], \STORAGE . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . $hash . '_' . $files['books']['name'][$i])) { + // Загружен и перемещён из временной папки файл с книгой + + // Запись в буфер сохранённых книг + $books[] = $hash . '_' . $files['books']['name'][$i]; + } + } + + foreach ($books as $book) { + // Перебор сохранённых книг + + // Запись в базу данных + + + // Инициализация пути до хранилища + $storage = \STORAGE . DIRECTORY_SEPARATOR . 'books' . DIRECTORY_SEPARATOR . 'id' . DIRECTORY_SEPARATOR; + + // Извлечение изображений из PDF-документа + exec("cd $storage && pdfimages -j $book"); + } + + // Перенаправление + header('Location: /books', response_code: 303); + + return null; + } + + /** + * Чтение + * + * @param array $vars Параметры запроса + * + * @return string|null HTML-документ + */ + public function read(array $vars = []): ?string + { + if (accounts::deauthentication($vars)) { + // Удалось деаутентифицироваться + } + + // Генерация представления + return $this->view->render(DIRECTORY_SEPARATOR . 'main' . DIRECTORY_SEPARATOR . 'index.html', $vars); + } + + /** + * Удаление + * + * @param array $vars Параметры запроса + * + * @return string|null HTML-документ + */ + public function delete(array $vars = []): ?string + { + if (accounts::deauthentication($vars)) { + // Удалось деаутентифицироваться + } + + // Генерация представления + return $this->view->render(DIRECTORY_SEPARATOR . 'main' . DIRECTORY_SEPARATOR . 'index.html', $vars); + } +} diff --git a/mirzaev/surikovlib/system/controllers/core.php b/mirzaev/surikovlib/system/controllers/core.php new file mode 100644 index 0000000..c538d5c --- /dev/null +++ b/mirzaev/surikovlib/system/controllers/core.php @@ -0,0 +1,34 @@ + + */ +class core extends controller +{ + /** + * Конструктор + * + * @return void + */ + public function __construct() + { + parent::__construct(); + + // Инициализация ядра моделей (соединение с базой данных...) + new models(); + + $this->view = new manager; + } +} diff --git a/mirzaev/surikovlib/system/controllers/errors_controller.php b/mirzaev/surikovlib/system/controllers/errors_controller.php new file mode 100644 index 0000000..4263048 --- /dev/null +++ b/mirzaev/surikovlib/system/controllers/errors_controller.php @@ -0,0 +1,31 @@ + + */ +final class errors_controller extends core +{ + public function error404(): ?string + { + // Генерация представления + return 'Ошибка 404 (не найдено)'; + } + + public function error500(): ?string + { + // Генерация представления + return 'Ошибка 500 (на стороне сервера)'; + } +} diff --git a/mirzaev/surikovlib/system/controllers/main_controller.php b/mirzaev/surikovlib/system/controllers/main_controller.php new file mode 100644 index 0000000..3ce6dec --- /dev/null +++ b/mirzaev/surikovlib/system/controllers/main_controller.php @@ -0,0 +1,29 @@ + + */ +final class main_controller extends core +{ + public function index(array $vars = []): ?string + { + // Проверка аутентифицированности + $vars['account'] = accounts::account($vars); + + // Генерация представления + return $this->view->render(DIRECTORY_SEPARATOR . 'main' . DIRECTORY_SEPARATOR . 'index.html', $vars); + } +} diff --git a/mirzaev/surikovlib/system/models/accounts_model.php b/mirzaev/surikovlib/system/models/accounts_model.php new file mode 100644 index 0000000..68203d9 --- /dev/null +++ b/mirzaev/surikovlib/system/models/accounts_model.php @@ -0,0 +1,621 @@ + + */ +final class accounts_model extends core +{ + /** + * Регистрация + * + * @param string $name Входной псевдоним + * @param string $email Почта + * @param string $password Пароль (password) + * @param bool $authentication Автоматическая аутентификация в случае успешной регистрации + * @param array &$errors Журнал ошибок + * + * @return array|bool Аккаунт, если удалось аутентифицироваться + */ + public static function registration(string $name = null, string $email = null, string $password, array &$errors = []): array + { + try { + if (static::account($errors)) { + // Аутентифицирован пользователь + + // Запись ошибки + throw new exception('Уже аутентифицирован'); + } + + if (empty($account = static::read(['name' => $name]) or $account = static::read(['email' => $email]))) { + // Не удалось найти аккаунт + + if (static::write($name, $email, $password, $errors)) { + // Удалось зарегистрироваться + + return $account; + } + } else { + // Удалось найти аккаунт + + return $account; + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return []; + } + + /** + * Аутентификация + * + * @param string $login Входной псевдоним + * @param string $password Пароль (password) + * @param bool $remember Функция "Запомнить меня" - увеличенное время хранения cookies + * @param array &$errors Журнал ошибок + * + * @return array Аккаунт (если не найден, то пустой массив) + */ + public static function authentication(string $login, string $password, bool $remember = false, array &$errors = []): array + { + try { + if (static::account($errors)) { + // Аутентифицирован пользователь + + // Запись ошибки + throw new exception('Уже аутентифицирован'); + } + + + if (empty($account = static::read(['name' => $login]) or $account = static::read(['email' => $login]))) { + // Не удалось найти аккаунт + + throw new exception('Не удалось найти аккаунт'); + } + + if (password_verify($password, $account['password'])) { + // Совпадают хеши паролей + + // Инициализация идентификатора сессии + session_id($account['id']); + + // Инициализация названия сессии + session_name('id'); + + // Инициализация сессии + session_start(); + + // Инициализация времени хранения хеша + $time = time() + ($remember ? 604800 : 86400); + + // Инициализация хеша + $hash = static::hash((int) $account['id'], crypt($account['password'], time() . $account['id']), $time, $errors)['hash']; + + // Инициализация cookies + setcookie("hash", $hash, $time, path: '/', secure: true); + + return $account; + } else { + // Не совпадают хеши паролей + + throw new exception('Неправильный пароль'); + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return []; + } + + /** + * Аутентификация + * + * @param array &$errors Журнал ошибок + * + * @return bool Удалось ли деаутентифицироваться + */ + public static function deauthentication(array &$errors = []): bool + { + try { + if ($account = static::account($errors)) { + // Аутентифицирован пользователь + + // Инициализация запроса + $request = static::$db->prepare("UPDATE `accounts` SET `hash` = null, `time` = 0 WHERE `id` = :id"); + + // Параметры запроса + $params = [ + ":id" => $account['id'], + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + $request->fetch(pdo::FETCH_ASSOC); + + // Деинициализация cookies + setcookie("id", '', 0, path: '/', secure: true); + setcookie("hash", '', 0, path: '/', secure: true); + + return true; + } else { + // Не аутентифицирован пользователь + + // Запись ошибки + throw new exception('Не аутентифицирован'); + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return false; + } + + /** + * Прочитать данные аккаунта, если пользователь аутентифицирован + * + * Можно использовать как проверку на аутентифицированность + * + * @param array &$errors Журнал ошибок + * + * @return array Аккаунт (если не найден, то пустой массив) + * + * @todo 1. Сделать в static::read() возможность передачи нескольких параметров и перенести туда непосредственно чтение аккаунта с проверкой хеша + */ + public static function account(array &$errors = []): array + { + try { + if (!empty($_COOKIE['id']) && !empty($_COOKIE['hash'])) { + // Аутентифицирован аккаунт (найдены cookie и они хранят значения - подразумевается, что не null или пустое) + + if ($_COOKIE['hash'] === static::hash((int) $_COOKIE['id'], errors: $errors)['hash']) { + // Совпадает переданный хеш с тем, что хранится в базе данных + } else { + // Не совпадает переданный хеш с тем, что хранится в базе данных + + // Генерация ошибки + throw new exception('Вы аутентифицированы с другого устройства (не совпадают хеши аутентификации)'); + } + + // Инициализация запроса + $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `id` = :id && `hash` = :hash"); + + // Параметры запроса + $params = [ + ":id" => $_COOKIE['id'], + ":hash" => $_COOKIE['hash'], + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + if (empty($account = $request->fetch(pdo::FETCH_ASSOC))) { + // Не найдена связка идентификатора с хешем + + // Генерация ошибки + throw new exception('Не найден пользотватель или время аутентификации истекло'); + } + + // Чтение разрешений + $account['permissions'] = static::permissions((int) $account['id'], $errors); + + return $account; + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return []; + } + + /** + * Прочитать разрешения аккаунта + * + * @param int $id Идентификатор аккаунта + * @param array &$errors Журнал ошибок + * + * @return array Разрешения аккаунта, если найдены + */ + public static function permissions(int $id, array &$errors = []): array + { + try { + // Инициализация запроса + $request = static::$db->prepare("SELECT * FROM `permissions` WHERE `id` = :id"); + + // Параметры запроса + $params = [ + ":id" => $id + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + if (empty($response = $request->fetch(pdo::FETCH_ASSOC))) { + // Не найдены разрешения + + // Генерация ошибки + throw new exception('Не найдены разрешения'); + } + + // Удаление ненужных данных + unset($response['id']); + + return $response; + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return []; + } + + /** + * Проверить разрешение + * + * @param string $permission Разрешение + * @param int|null $id Идентификатор аккаунта + * @param array &$errors Журнал ошибок + * + * @return bool|null Статус разрешения, если оно записано + */ + public static function access(string $permission, int|null $id = null, array &$errors = []): ?bool + { + try { + // Инициализация аккаунта + $account = isset($id) ? self::read(['id' => $id], $errors) : self::account($errors); + + return isset($account['permissions'][$permission]) ? (bool) $account['permissions'][$permission] : null; + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return null; + } + + /** + * Запись пользователя в базу данных + * + * @param string|null $name Имя + * @param string|null $email Почта + * @param string|null $password Пароль + * @param array &$errors Журнал ошибок + * + * @return array Аккаунт (если не найден, то пустой массив) + */ + public static function write(string|null $name = null, string|null $email = null, string|null $password = null, array &$errors = []): array + { + try { + // Инициализация параметров запроса + $params = []; + + if (isset($name)) { + try { + // Проверка параметра + if (iconv_strlen($name) < 3) throw new exception('Длина имени должна быть не менее 3 символов'); + if (iconv_strlen($name) > 60) throw new exception('Длина имени должна быть не более 60 символов'); + + // Запись в буфер параметров запроса + $params[':name'] = $name; + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]; + + goto end; + } + } + + if (isset($email)) { + try { + // Проверка параметра + if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) throw new exception('Не удалось распознать почту'); + if (iconv_strlen($email) < 3) throw new exception('Длина почты должна быть не менее 3 символов'); + if (iconv_strlen($email) > 60) throw new exception('Длина почты должна быть не более 80 символов'); + + // Запись в буфер параметров запроса + $params[':email'] = $email; + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]; + + goto end; + } + } + + if (isset($password)) { + try { + // Проверка параметра + if (iconv_strlen($password) < 3) throw new exception('Длина пароля должна быть не менее 3 символов'); + if (iconv_strlen($password) > 60) throw new exception('Длина пароля должна быть не более 120 символов'); + + // Запись в буфер параметров запроса + $params[':password'] = password_hash($password, PASSWORD_BCRYPT); + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]; + + goto end; + } + } + + // Инициализация запроса + $request = static::$db->prepare("INSERT INTO `accounts` (" . (isset($name) ? '`name`' : '') . (isset($name) && isset($email) ? ', ' : '') . (isset($email) ? '`email`' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? '`password`' : '') . ") VALUES (" . (isset($name) ? ':name' : '') . (isset($name) && isset($email) ? ', ' : '') . (isset($email) ? ':email' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? ':password' : '') . ")"); + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + $request->fetch(pdo::FETCH_ASSOC); + + try { + if (isset($name)) { + // Передано имя аккаунта + + // Чтение аккаунта + $account = static::read(['name' => $name]); + } else if (isset($email)) { + // Передана почта аккаунта + + // Чтение аккаунта + $account = static::read(['email' => $email]); + } else { + // Не передано ни имя, ни почта + + throw new exception('Не переданны данные для полноценной регистрации аккаунта'); + } + + // Инициализация запроса + $request = static::$db->prepare("INSERT INTO `permissions` (`id`) VALUES (:id)"); + + // Инициализация параметров + $params = [ + ':id' => $account['id'] + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + $request->fetch(pdo::FETCH_ASSOC); + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + // Конец выполнения + end: + + return isset($account) && $account ? $account : []; + } + + /** + * Чтение пользователя из базы данных + * + * @param array $search Поиск ('поле' => 'значение'), работает только с одним полем + * @param array &$errors Журнал ошибок + * + * @return array Аккаунт, если найден + */ + public static function read(array $search, array &$errors = []): array + { + try { + // Инициализация данных для поиска + $field = array_keys($search)[0] ?? null; + $value = $search[$field] ?? null; + + if (empty($field)) { + // Получено пустое значение поля + + // Запись ошибки + throw new exception('Пустое значение поля для поиска'); + } + + // Инициализация запроса + $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `$field` = :field LIMIT 1"); + + // Параметры запроса + $params = [ + ":field" => $value, + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + if ($account = $request->fetch(pdo::FETCH_ASSOC)) { + // Найден аккаунт + + try { + if ($permissions = static::permissions((int) $account['id'], $errors)) { + // Найдены разрешения + + // Запись в буфер данных аккаунта + $account['permissions'] = $permissions; + } else { + // Не найдены разрешения + + throw new exception('Не удалось найти и прочитать разрешения'); + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]; + } + } else { + // Не найден аккаунт + + throw new exception('Не удалось найти аккаунт'); + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return isset($account) && $account ? $account : []; + } + + /** + * Запись или чтение хеша из базы данных + * + * @param int $id Идентификатор аккаунта + * @param int|null $hash Хеш аутентифиакции + * @param string|null $time Время хранения хеша + * @param array &$errors Журнал ошибок + * + * @return array ['hash' => $hash, 'time' => $time] + */ + public static function hash(int $id, string|null $hash = null, int|null $time = null, array &$errors = []): array + { + try { + if (isset($hash, $time)) { + // Переданы хеш и его время хранения + + // Инициализация запроса + $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id"); + + // Параметры запроса + $params = [ + ":id" => $id, + ":hash" => $hash, + ":time" => $time, + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + $request->fetch(pdo::FETCH_ASSOC); + } else { + // Не переданы хеш и его время хранения + + // Инициализация запроса + $request = static::$db->prepare("SELECT `hash`, `time` FROM `accounts` WHERE `id` = :id"); + + // Параметры запроса + $params = [ + ":id" => $id, + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + extract((array) $request->fetch(pdo::FETCH_ASSOC)); + + if (!empty($response['time']) && $response['time'] <= time()) { + // Истекло время жизни хеша + + // Инициализация запроса + $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id"); + + // Параметры запроса + $params = [ + ":id" => $id, + ":hash" => null, + ":time" => null, + ]; + + // Отправка запроса + $request->execute($params); + + // Генерация ответа + $response = $request->fetch(pdo::FETCH_ASSOC); + + // Генерация ошибки + throw new exception('Время аутентификации истекло'); + } + } + } catch (exception $e) { + // Запись в журнал ошибок + $errors[]= [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + + return ['hash' => $hash, 'time' => $time]; + } +} diff --git a/mirzaev/surikovlib/system/models/books_model.php b/mirzaev/surikovlib/system/models/books_model.php new file mode 100644 index 0000000..2accf4f --- /dev/null +++ b/mirzaev/surikovlib/system/models/books_model.php @@ -0,0 +1,49 @@ + + */ +final class books_model extends core +{ + public static function read(array $where = [], int $limit = 1, int $page = 1): array + { + // Инициализация строки поиска + $row = ''; + + // Инициализация параметров запроса + $params = []; + + foreach ($where as $parameter => $value) { + // Перебор параметров поиска + + // Запись функции + if(empty($row)) $row = 'WHERE '; + + // Запись в строку поиска + $row .= "`$parameter` = :$parameter"; + + $params[$parameter] = $value; + } + + // Инициализация страницы + $page = $limit * --$page; + + // Инициализация запроса + $request = static::$db->prepare("SELECT * FROM `books` $row LIMIT $page, $limit"); + + // Отправка запроса + $request->execute($params); + + return (array) $request->fetchAll(pdo::FETCH_ASSOC); + } +} diff --git a/mirzaev/surikovlib/system/models/core.php b/mirzaev/surikovlib/system/models/core.php new file mode 100644 index 0000000..fb5326f --- /dev/null +++ b/mirzaev/surikovlib/system/models/core.php @@ -0,0 +1,139 @@ + + */ +class core extends model +{ + /** + * Соединение с базой данных + */ + protected static PDO $db ; + + public function __construct(pdo $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 pdo) { + // Передано подходящее значение + + // Запись свойства (успех) + self::$db = $value; + } else { + // Передано неподходящее значение + + // Выброс исключения (неудача) + throw new exception('Соединение с базой данных ($this->db) должен быть инстанцией PDO', 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 pdo(\TYPE . ':dbname=' . \BASE . ';host=' . \HOST, LOGIN, PASSWORD)); + } + + 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/surikovlib/system/public/Nginx_1.21_vhost.conf b/mirzaev/surikovlib/system/public/Nginx_1.21_vhost.conf new file mode 100644 index 0000000..2ea3356 --- /dev/null +++ b/mirzaev/surikovlib/system/public/Nginx_1.21_vhost.conf @@ -0,0 +1,105 @@ +# ---------------------------- +# Host config +# ---------------------------- + +server { + + listen %ip%:%httpport%; + listen %ip%:%httpsport% ssl http2; + + server_name surikovlib.loc %aliases%; + root '%hostdir%'; + limit_conn addr 64; + autoindex off; + index index.php index.html index.htm; + + ssl_certificate '%sprogdir%/userdata/config/cert_files/server.crt'; + ssl_certificate_key '%sprogdir%/userdata/config/cert_files/server.key'; + # ssl_trusted_certificate ''; + + # Force HTTPS + # add_header Strict-Transport-Security 'max-age=2592000' always; + # if ($scheme ~* ^(?!https).*$) { + # return 301 https://$host$request_uri; + # } + + # Force www.site.com => site.com + # if ($host ~* ^www\.(.+)$) { + # return 301 $scheme://$1$request_uri; + # } + # Disable access to backup/config/command/log files + # if ($uri ~* ^.+\.(?:bak|co?nf|in[ci]|log|orig|sh|sql|tar|sql|t?gz|cmd|bat)$) { + # return 404; + # } + # Disable access to hidden files/folders + if ($uri ~* /\.(?!well-known)) { + return 404; + } + + # Disable MIME sniffing + add_header X-Content-Type-Options 'nosniff' always; + + location ~* ^.+\.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv|svgz?|ttf|ttc|otf|eot|woff2?)$ { + expires 1d; + access_log off; + } + + location / { + # Force index.php routing (if not found) + try_files $uri $uri/ /index.php$is_args$args; + + # Force index.php routing (all requests) + # rewrite ^/(.*)$ /index.php?/$1 last; + + location ~ \.php$ { + try_files $fastcgi_script_name =404; + + # limit_conn addr 16; + # limit_req zone=flood burst=32 nodelay; + + # add_header X-Frame-Options 'SAMEORIGIN' always; + # add_header Referrer-Policy 'no-referrer-when-downgrade' always; + # CSP syntax: (http: https: data: mediastream: blob: filesystem:) 'self' 'unsafe-inline' 'unsafe-eval' 'none' + # Content-Security-Policy-Report-Only (report-uri https://site.com/csp/) + # add_header Content-Security-Policy "default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always; + fastcgi_pass backend; + include '%sprogdir%/userdata/config/nginx_fastcgi_params.txt'; + } + } + + # Service configuration (do not edit!) + # ---------------------------- + location /openserver/ { + root '%sprogdir%/modules/system/html'; + autoindex off; + index index.php index.html index.htm; + + %allow%allow all; + allow 127.0.0.0/8; + allow ::1/128; + allow %ips%; + deny all; + + location ~* ^/openserver/.+\.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv|svgz?|ttf|ttc|otf|eot|woff2?)$ { + expires 1d; + access_log off; + } + + location /openserver/server-status { + stub_status on; + } + + location ~ ^/openserver/.*\.php$ { + try_files $fastcgi_script_name =404; + fastcgi_index index.php; + fastcgi_pass backend; + include '%sprogdir%/userdata/config/nginx_fastcgi_params.txt'; + } + } + # End service configuration + # ---------------------------- +} + +# ---------------------------- +# End host config +# ---------------------------- diff --git a/mirzaev/surikovlib/system/public/css/auth.css b/mirzaev/surikovlib/system/public/css/auth.css new file mode 100644 index 0000000..cbbc18e --- /dev/null +++ b/mirzaev/surikovlib/system/public/css/auth.css @@ -0,0 +1,67 @@ +#authentication>:is(form, div) { + display: flex; + flex-direction: column; +} + +#authentication .exit { + margin-top: 25px; +} + +#authentication p { + margin: 0; + display: flex; +} + +#authentication p>span { + margin-left: auto; +} + +#authentication>form>input:is([type="text"], [type="password"]) { + margin-bottom: 12px; +} + +#authentication>form>input:last-child { + margin-bottom: unset; +} + +#authentication>form>.submit { + margin-top: 6px; + margin-bottom: 10px; + display: flex; +} + +#authentication>form>.submit>label { + padding: 10px 20px; + border: unset; + border-radius: 3px 0 0 3px; +} + +#authentication>form>.submit>input { + padding: 10px 20px; + flex-grow: 1; + border: unset; + border-radius: 0 3px 3px 0; +} + + +#authentication>form>input[type=submit].registration { + padding: 7px 20px; + background-color: #86781C; +} + +#authentication>form>input[type=submit].registration:hover { + background-color: #9e8d20; +} + +#authentication>form>input[type=submit].registration:is(:active, :focus) { + background-color: #776b19; +} + +#authentication>form>ul.errors { + margin-top: 18px; + margin-bottom: 0px; + padding: 10px; + text-align: center; + list-style: none; + background-color: #ae8f8f; +} diff --git a/mirzaev/surikovlib/system/public/css/books.css b/mirzaev/surikovlib/system/public/css/books.css new file mode 100644 index 0000000..151e35d --- /dev/null +++ b/mirzaev/surikovlib/system/public/css/books.css @@ -0,0 +1,52 @@ +main>section#books { + display: flex; + flex-flow: row wrap; +} + +main>section#books>* { + margin-bottom: 20px; +} + +main>section#books>form.upload { + width: calc(100% / 3 - 20px - 9px * 2); + height: calc(220px - 9px * 2); + margin: 5px; + margin-right: 20px; +} + +main>section#books>form.upload>p { + font-size: 3rem; + height: 0.3rem; + line-height: 0; +} + +main>section#books>article.book { + width: calc(100% / 3 - 20px); + margin-right: 20px; + display: flex; + flex-direction: column; +} + +main>section#books>article.book:nth-child(3) { + width: calc(100% / 3); + margin-right: unset; +} + +main>section#books>article.book>img { + height: 220px; + object-fit: cover; + object-position: right; + overflow: hidden; + clip-path: polygon(5px calc(100% - 5px), calc(100% - 5px) calc(100% - 5px), calc(100% - 5px) 5px, 5px 5px); +} + +main>section#books>article.book>h4 { + margin-top: 5px; + margin-bottom: 10px; + height: 50px; + text-align: center; +} + +main>section#books>article.book>p { + margin: unset; +} diff --git a/mirzaev/surikovlib/system/public/css/main.css b/mirzaev/surikovlib/system/public/css/main.css new file mode 100644 index 0000000..36017c1 --- /dev/null +++ b/mirzaev/surikovlib/system/public/css/main.css @@ -0,0 +1,262 @@ +* { + font-family: "open sans"; + text-decoration: none; +} + +body { + margin: 0; + display: grid; + grid-template-rows: auto auto 150px; + grid-template-columns: auto 300px 600px auto; + background-color: #e5ddd1; +} + +a { + color: unset; +} + +.content { + display: flex; + padding: 0px 20vw; +} + +aside { + margin-right: 20px; + grid-column: 2; +} + +aside>section { + margin-bottom: 15px; +} + +aside>section>h3 { + margin-top: -0.2rem; + text-align: center; +} + +:is(main, aside) { + margin-top: 30px; + display: inline-block; + grid-row: 2; +} + +:is(main, aside)>section { + border-radius: 3px; + background-color: #d9b5b5; +} + +:is(main, aside)>section { + padding: 20px; +} + +main { + grid-column: 3; +} + +main>section>h2 { + margin-left: 1.5rem; + margin-top: unset; + font-size: 1.8rem; +} + +/* main>section>h2:has(+ .divider) { */ +main>section>h2+.divider { + margin-top: -1rem; +} + +header { + z-index: 1000; + margin-bottom: 30px; + grid-row: 1; +} + +header>.menu { + z-index: 1000; + width: 100%; + height: 250px; + position: absolute; + display: flex; +} + +header>.menu>nav { + top: 0; + margin: 0 auto; + height: 40px; + padding: 15px 0; + position: sticky; + display: flex; +} + +header>.menu>nav>#logo { + height: inherit; +} + +header>.menu>nav>#logo>img { + height: inherit; + position: relative; +} + +header>.menu>nav>#logo~.link { + margin-right: unset; + margin-left: 50px; +} + +header>.menu>nav>.link { + margin: auto 0; + margin-right: 50px; + text-decoration: none; + color: #e5ddd1; +} + +header>.menu>nav>.link:last-child { + margin-right: auto; +} + +header>.window { + z-index: 500; + height: 650px; + position: relative; + display: flex; + padding: 30px; + overflow: hidden; + background-color: #1a1449; +} + +header>.window>.background { + margin-top: auto; + width: 100%; + height: min-content; +} + +header>.window>.img_1 { + left: -70px; + bottom: -20px; + height: 120%; + position: absolute; +} + +header, +footer { + grid-column-start: 1; + grid-column-end: 5; +} + +header>nav, +footer { + background-color: #1a1449; +} + +footer { + z-index: 800; + margin-top: 30px; + width: 100%; + height: 100%; + grid-row: 3; +} + +.banners { + height: 100%; +} + +.banners>img { + width: 100%; + position: sticky; +} + +.banner { + width: 100%; +} + +.button, +:is(a, label)[type="button"], +input[type="submit"] { + padding: 10px 20px; + cursor: pointer; + text-align: center; + border: unset; + border-radius: 3px; + color: #fdfdfd; + background-color: #AD4717; +} + +.button:hover, +:is(a, label)[type="button"]:hover, +input[type="submit"]:hover { + color: #fff; + background-color: #c5531f; +} + +.button:active:is(:active, :focus), +:is(a, label)[type="button"]:is(:active, :focus), +input[type="radio"]:checked+label[type="button"], +input[type="submit"]:is(:active, :focus) { + color: #ddd; + background-color: #993f15; +} + +input:is([type="checkbox"], [type="radio"]) { + display: none; +} + +select, +input:is([type="text"], [type="password"]), +input:is([type="text"], [type="password"]).measured+.unit { + padding: 8px 12px; + outline: unset; + border-radius: 3px; + border: unset; +} + +select { + padding-top: unset; + padding-bottom: unset; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; +} + +select.measured, +input:is([type="text"], [type="password"]).measured { + margin-right: unset; + padding-right: 3px; + text-align: right; + border-radius: 3px 0 0 3px; +} + +select.measured+.unit, +input:is([type="text"], [type="password"]).measured+.unit { + margin-right: 3px; + padding-left: unset; + display: inline; + border-radius: 0 3px 3px 0; + background-color: #fff; +} + +.unit { + display: none; +} + +.unselectable, +.unselectable * { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.divider { + width: 100%; + border-radius: 2px; +} + +.divider+h3 { + text-align: center; +} + +section>div.divider { + margin-bottom: 1rem; + border-bottom: 3px solid #aaa9a9; +} diff --git a/mirzaev/surikovlib/system/public/css/upload.css b/mirzaev/surikovlib/system/public/css/upload.css new file mode 100644 index 0000000..6d91f5f --- /dev/null +++ b/mirzaev/surikovlib/system/public/css/upload.css @@ -0,0 +1,33 @@ +form.upload { + width: 100%; + height: 100px; + position: relative; + display: flex; + border: 4px dashed #e5ddd1; +} + +form.upload:hover { + background-color: #ccc6bd; + border: 4px dashed #fff7ea; +} + +form.upload>p { + margin: auto; + font-weight: bold; + color: #eee6d9; +} + +form.upload:hover>p { + color: #fff7ea; +} + +form.upload>input { + width: 100%; + height: 100%; + position: absolute; + opacity: 0; +} + +form.upload:hover>input { + cursor: pointer; +} diff --git a/mirzaev/surikovlib/system/public/img/background_1.png b/mirzaev/surikovlib/system/public/img/background_1.png new file mode 100644 index 0000000..36f12a3 Binary files /dev/null and b/mirzaev/surikovlib/system/public/img/background_1.png differ diff --git a/mirzaev/surikovlib/system/public/img/background_1.svg b/mirzaev/surikovlib/system/public/img/background_1.svg new file mode 100644 index 0000000..f588d60 --- /dev/null +++ b/mirzaev/surikovlib/system/public/img/background_1.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mirzaev/surikovlib/system/public/img/background_2.png b/mirzaev/surikovlib/system/public/img/background_2.png new file mode 100644 index 0000000..3a6c7ee Binary files /dev/null and b/mirzaev/surikovlib/system/public/img/background_2.png differ diff --git a/mirzaev/surikovlib/system/public/img/surikovlib_logo_1.svg b/mirzaev/surikovlib/system/public/img/surikovlib_logo_1.svg new file mode 100644 index 0000000..049c2f3 --- /dev/null +++ b/mirzaev/surikovlib/system/public/img/surikovlib_logo_1.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mirzaev/surikovlib/system/public/img/surikovlib_logo_1_white.svg b/mirzaev/surikovlib/system/public/img/surikovlib_logo_1_white.svg new file mode 100644 index 0000000..482612d --- /dev/null +++ b/mirzaev/surikovlib/system/public/img/surikovlib_logo_1_white.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mirzaev/surikovlib/system/public/img/ФЖ-28.png b/mirzaev/surikovlib/system/public/img/ФЖ-28.png new file mode 100644 index 0000000..d93c0b0 Binary files /dev/null and b/mirzaev/surikovlib/system/public/img/ФЖ-28.png differ diff --git a/mirzaev/surikovlib/system/public/img/школьникам.png b/mirzaev/surikovlib/system/public/img/школьникам.png new file mode 100644 index 0000000..07fcf4b Binary files /dev/null and b/mirzaev/surikovlib/system/public/img/школьникам.png differ diff --git a/mirzaev/surikovlib/system/public/index.php b/mirzaev/surikovlib/system/public/index.php new file mode 100644 index 0000000..0e2bcbd --- /dev/null +++ b/mirzaev/surikovlib/system/public/index.php @@ -0,0 +1,40 @@ +write('/', 'main', 'index'); +$router->write('/account/registration', 'accounts', 'registration', 'POST'); +$router->write('/account/authentication', 'accounts', 'authentication', 'POST'); +$router->write('/account/deauthentication', 'accounts', 'deauthentication', 'POST'); +$router->write('/account/deauthentication', 'accounts', 'deauthentication', 'GET'); +$router->write('/books', 'books', 'index', 'GET'); +$router->write('/books/$id', 'books', 'index', 'GET'); +$router->write('/books/$id/cover', 'books', 'cover', 'GET'); +$router->write('/books/$id/0', 'books', 'cover', 'GET'); +$router->write('/books/write', 'books', 'write', 'POST'); + +// Инициализация ядра +$core = new core(namespace: __NAMESPACE__, router: $router); + +// Обработка запроса +echo $core->start(); diff --git a/mirzaev/surikovlib/system/public/js/auth.js b/mirzaev/surikovlib/system/public/js/auth.js new file mode 100644 index 0000000..1e72fc8 --- /dev/null +++ b/mirzaev/surikovlib/system/public/js/auth.js @@ -0,0 +1,37 @@ +'use strict'; + +function remember_switch(target) { + if (target.classList.contains('fa-unlock')) { + // Найден "открытый замок" + + // Перезапись на "закрытый замок" + target.classList.remove('fa-unlock'); + target.classList.add('fa-lock'); + + // Изменение отправляемого значения + document.querySelector('input[name=' + target.getAttribute('for') + ']').checked = true; + } else { + // Не найден "открытый замок", подразумевается, что найден "закрытый замок" + + // Перезапись на "открытый замок" + target.classList.remove('fa-lock'); + target.classList.add('fa-unlock'); + + // Изменение отправляемого значения + document.querySelector('input[name=' + target.getAttribute('for') + ']').checked = false; + } +} + +function authentication(form) { + // Инициализация адреса отправки формы + form.action = '/account/authentication'; + + return true; +} + +function registration(form) { + // Инициализация адреса отправки формы + form.action = '/account/registration'; + + return true; +} diff --git a/mirzaev/surikovlib/system/storage/.gitignore b/mirzaev/surikovlib/system/storage/.gitignore new file mode 100644 index 0000000..b0ea3d6 --- /dev/null +++ b/mirzaev/surikovlib/system/storage/.gitignore @@ -0,0 +1,2 @@ +/books/* +/temp/* \ No newline at end of file diff --git a/mirzaev/surikovlib/system/views/auth.html b/mirzaev/surikovlib/system/views/auth.html new file mode 100644 index 0000000..d04af42 --- /dev/null +++ b/mirzaev/surikovlib/system/views/auth.html @@ -0,0 +1,38 @@ +
+ + + {% if account is not empty %} +

Аккаунт

+
+

Почта: {{ account.email }}

+ Выход +
+ {% else %} +

Аутентификация

+
+ + + +
+ + + +
+ + + {% if errors is not empty %} + {% if errors.account is not empty %} +
    + {% for error in errors.account %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + {% endif %} +
+ {% endif %} + + +
diff --git a/mirzaev/surikovlib/system/views/books/book.html b/mirzaev/surikovlib/system/views/books/book.html new file mode 100644 index 0000000..786df82 --- /dev/null +++ b/mirzaev/surikovlib/system/views/books/book.html @@ -0,0 +1,11 @@ +{% extends "core.html" %} + +{% block main %} + + +
+ Обложка книги "{{ book.title|e }}" +

{{ book.title|e }}

+

{{ book.description|e|length|trim(' ') > 80 ? book.description|e|slice(0, 80)|trim(' ') ~ '...' : book.description|e}}

+
+{% endblock %} diff --git a/mirzaev/surikovlib/system/views/books/index.html b/mirzaev/surikovlib/system/views/books/index.html new file mode 100644 index 0000000..67b8f1f --- /dev/null +++ b/mirzaev/surikovlib/system/views/books/index.html @@ -0,0 +1,19 @@ +{% extends "core.html" %} + +{% block main %} + + + +
+
+ +

+

+
+ {% for book in books %} + + {% endfor %} +
+{% endblock %} diff --git a/mirzaev/surikovlib/system/views/core.html b/mirzaev/surikovlib/system/views/core.html new file mode 100644 index 0000000..49979f2 --- /dev/null +++ b/mirzaev/surikovlib/system/views/core.html @@ -0,0 +1,34 @@ + + + + + + {% include 'head.html' %} + + + + + {% block title %} + Библеотека Сурикова + {% endblock %} + + + + + {% include 'header.html' %} + +
+ {% block main %} + {% include 'calculators/index.html' %} + {% endblock %} +
+ + {% include 'footer.html' %} + + + {% include 'js.html' %} + + + diff --git a/mirzaev/surikovlib/system/views/footer.html b/mirzaev/surikovlib/system/views/footer.html new file mode 100644 index 0000000..0ed3a6c --- /dev/null +++ b/mirzaev/surikovlib/system/views/footer.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/mirzaev/surikovlib/system/views/head.html b/mirzaev/surikovlib/system/views/head.html new file mode 100644 index 0000000..0f4705e --- /dev/null +++ b/mirzaev/surikovlib/system/views/head.html @@ -0,0 +1,2 @@ + + diff --git a/mirzaev/surikovlib/system/views/header.html b/mirzaev/surikovlib/system/views/header.html new file mode 100644 index 0000000..e014806 --- /dev/null +++ b/mirzaev/surikovlib/system/views/header.html @@ -0,0 +1,18 @@ +
+ + +
+ + +
+
diff --git a/mirzaev/surikovlib/system/views/js.html b/mirzaev/surikovlib/system/views/js.html new file mode 100644 index 0000000..0016c78 --- /dev/null +++ b/mirzaev/surikovlib/system/views/js.html @@ -0,0 +1,9 @@ + + + + + + + +{% block js %} +{% endblock %} diff --git a/mirzaev/surikovlib/system/views/main/index.html b/mirzaev/surikovlib/system/views/main/index.html new file mode 100644 index 0000000..f113312 --- /dev/null +++ b/mirzaev/surikovlib/system/views/main/index.html @@ -0,0 +1,5 @@ +{% extends "core.html" %} + +{% block main %} + +{% endblock %} diff --git a/mirzaev/surikovlib/system/views/manager.php b/mirzaev/surikovlib/system/views/manager.php new file mode 100644 index 0000000..62485ee --- /dev/null +++ b/mirzaev/surikovlib/system/views/manager.php @@ -0,0 +1,25 @@ + + */ +final class manager extends controller +{ + public function render(string $file, array $vars = []): ?string + { + // Генерация представления + return (new view(new FilesystemLoader(VIEWS)))->render($file, $vars); + } +} diff --git a/mirzaev/surikovlib/system/views/sidebar.html b/mirzaev/surikovlib/system/views/sidebar.html new file mode 100644 index 0000000..faec26c --- /dev/null +++ b/mirzaev/surikovlib/system/views/sidebar.html @@ -0,0 +1,6 @@ + + +{% include 'auth.html' %} +{% include 'vk.html' %} + diff --git a/mirzaev/surikovlib/system/views/vk.1.html b/mirzaev/surikovlib/system/views/vk.1.html new file mode 100644 index 0000000..f1f5959 --- /dev/null +++ b/mirzaev/surikovlib/system/views/vk.1.html @@ -0,0 +1,8 @@ + + + +
+ + diff --git a/mirzaev/surikovlib/system/views/vk.html b/mirzaev/surikovlib/system/views/vk.html new file mode 100644 index 0000000..41c9c63 --- /dev/null +++ b/mirzaev/surikovlib/system/views/vk.html @@ -0,0 +1,10 @@ +
+ + + +
+ + +