From cba871c5df9f7120c44d9952456ebbd0d220669e Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Wed, 12 Aug 2020 20:03:45 +0000 Subject: [PATCH] feat: add install wizard form to bootstrap database and create the first superadmin user - generate .env file to configure instance's environment - add phpdotenv dependency to verify .env file - add AppSeeder to call all required seeds at once - add env and superadmin form views using form helpers closes #2 --- DEPENDENCIES.md | 1 + app/Config/App.php | 6 +- app/Config/Database.php | 2 +- app/Config/Routes.php | 14 +- app/Controllers/Install.php | 270 +++++++++++++++ app/Controllers/Migrate.php | 25 -- app/Database/Seeds/AppSeeder.php | 25 ++ app/Language/en/AdminNavigation.php | 2 +- app/Language/en/Install.php | 42 +++ app/Views/admin/_header.php | 2 +- app/Views/admin/_sidenav.php | 2 +- app/Views/install/_layout.php | 26 ++ app/Views/install/env.php | 89 +++++ app/Views/install/error.php | 9 + app/Views/install/superadmin.php | 45 +++ composer.json | 3 +- composer.lock | 514 +++++++++++++++++++++++----- public/favicon.ico | Bin 3758 -> 3758 bytes 18 files changed, 965 insertions(+), 112 deletions(-) create mode 100644 app/Controllers/Install.php delete mode 100644 app/Controllers/Migrate.php create mode 100644 app/Database/Seeds/AppSeeder.php create mode 100644 app/Language/en/Install.php create mode 100644 app/Views/install/_layout.php create mode 100644 app/Views/install/env.php create mode 100644 app/Views/install/error.php create mode 100644 app/Views/install/superadmin.php diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 62ea05ab..612c3284 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -11,6 +11,7 @@ PHP Dependencies: - [getID3](https://github.com/JamesHeinrich/getID3) ([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt)) - [myth-auth](https://github.com/lonnieezell/myth-auth) ([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md)) - [commonmark](https://commonmark.thephpleague.com/) ([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE)) +- [phpdotenv](https://github.com/vlucas/phpdotenv) ([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE)) Javascript dependencies: diff --git a/app/Config/App.php b/app/Config/App.php index 55952114..f05de3d8 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -34,7 +34,7 @@ class App extends BaseConfig | variable so that it is blank. | */ - public $indexPage = 'index.php'; + public $indexPage = ''; /* |-------------------------------------------------------------------------- @@ -281,7 +281,7 @@ class App extends BaseConfig |-------------------------------------------------------------------------- | Defines a base route for all admin pages */ - public $adminGateway = 'admin'; + public $adminGateway = 'cp-admin'; /* |-------------------------------------------------------------------------- @@ -289,5 +289,5 @@ class App extends BaseConfig |-------------------------------------------------------------------------- | Defines a base route for all authentication related pages */ - public $authGateway = 'auth'; + public $authGateway = 'cp-auth'; } diff --git a/app/Config/Database.php b/app/Config/Database.php index a77c5865..40853675 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -38,7 +38,7 @@ class Database extends \CodeIgniter\Database\Config 'password' => '', 'database' => '', 'DBDriver' => 'MySQLi', - 'DBPrefix' => '', + 'DBPrefix' => 'cp_', 'pConnect' => false, 'DBDebug' => ENVIRONMENT !== 'production', 'cacheOn' => false, diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f65494dd..dc8d3ce9 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -31,7 +31,6 @@ $routes->setAutoRoute(false); $routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}'); $routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}'); -$routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}'); /** * -------------------------------------------------------------------- @@ -43,6 +42,17 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}'); // route since we don't have to scan directories. $routes->get('/', 'Home::index', ['as' => 'home']); +// Install Wizard route +$routes->group('cp-install', function ($routes) { + $routes->get('/', 'Install', ['as' => 'install']); + $routes->post('generate-env', 'Install::attemptCreateEnv', [ + 'as' => 'install_generate_env', + ]); + $routes->post('create-superadmin', 'Install::attemptCreateSuperAdmin', [ + 'as' => 'install_create_superadmin', + ]); +}); + // Public routes $routes->group('@(:podcastName)', function ($routes) { $routes->get('/', 'Podcast/$1', ['as' => 'podcast']); @@ -68,7 +78,7 @@ $routes->group( ['namespace' => 'App\Controllers\Admin'], function ($routes) { $routes->get('/', 'Home', [ - 'as' => 'admin_home', + 'as' => 'admin', ]); $routes->get('my-podcasts', 'Podcast::myPodcasts', [ diff --git a/app/Controllers/Install.php b/app/Controllers/Install.php new file mode 100644 index 00000000..fd5f4f68 --- /dev/null +++ b/app/Controllers/Install.php @@ -0,0 +1,270 @@ +load(); + $dotenv->required([ + 'app.baseURL', + 'app.adminGateway', + 'app.authGateway', + 'database.default.hostname', + 'database.default.database', + 'database.default.username', + 'database.default.password', + 'database.default.DBPrefix', + ]); + } catch (\Throwable $e) { + // Invalid .env file + return $this->createEnv(); + } + + // Check if database configuration is ok + try { + $db = db_connect(); + + // Check if superadmin has been created, meaning migrations and seeds have passed + if ( + $db->tableExists('users') && + (new UserModel())->countAll() > 0 + ) { + // if so, show a 404 page + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + } catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) { + // return an error view to + return view('install/error', [ + 'error' => lang('Install.messages.databaseConnectError'), + ]); + } + + // migrate if no user has been created + $this->migrate(); + + // Check if all seeds have succeeded + $this->seed(); + + return $this->createSuperAdmin(); + } + + /** + * Returns the form to generate the .env config file for the instance. + */ + public function createEnv() + { + helper('form'); + + return view('install/env'); + } + + /** + * Verifies that all fields have been submitted correctly and + * creates the .env file after user submits the install form. + */ + public function attemptCreateEnv() + { + if ( + !$this->validate([ + 'hostname' => 'required|valid_url', + 'admin_gateway' => 'required|differs[auth_gateway]', + 'auth_gateway' => 'required|differs[admin_gateway]', + 'db_hostname' => 'required', + 'db_name' => 'required', + 'db_username' => 'required', + 'db_password' => 'required', + ]) + ) { + return redirect() + ->back() + ->with('errors', $this->validator->getErrors()); + } + + // Create .env file with post data + try { + $envFile = fopen(ROOTPATH . '.env', 'w'); + if (!$envFile) { + throw new Exception('File open failed.'); + } + + $envMapping = [ + [ + 'key' => 'app.baseURL', + 'value' => $this->request->getPost('hostname'), + ], + [ + 'key' => 'app.adminGateway', + 'value' => $this->request->getPost('admin_gateway'), + ], + [ + 'key' => 'app.authGateway', + 'value' => $this->request->getPost('auth_gateway'), + ], + [ + 'key' => 'database.default.hostname', + 'value' => $this->request->getPost('db_hostname'), + ], + [ + 'key' => 'database.default.database', + 'value' => $this->request->getPost('db_name'), + ], + [ + 'key' => 'database.default.username', + 'value' => $this->request->getPost('db_username'), + ], + [ + 'key' => 'database.default.password', + 'value' => $this->request->getPost('db_password'), + ], + [ + 'key' => 'database.default.DBPrefix', + 'value' => $this->request->getPost('db_prefix'), + ], + ]; + + foreach ($envMapping as $envVar) { + if ($envVar['value']) { + fwrite( + $envFile, + $envVar['key'] . '="' . $envVar['value'] . '"' . PHP_EOL + ); + } + } + + return redirect()->back(); + } catch (\Throwable $e) { + return redirect() + ->back() + ->with('error', $e->getMessage()); + } finally { + fclose($envFile); + } + } + + /** + * Runs all database migrations required for instance. + */ + public function migrate() + { + $migrations = \Config\Services::migrations(); + + if ( + !$migrations->setNamespace('Myth\Auth')->latest() or + !$migrations->setNamespace(APP_NAMESPACE)->latest() + ) { + return view('install/error', [ + 'error' => lang('Install.messages.migrationError'), + ]); + } + } + + /** + * Runs all database seeds required for instance. + */ + public function seed() + { + try { + $seeder = \Config\Database::seeder(); + + // Seed database + $seeder->call('AppSeeder'); + } catch (\Throwable $e) { + return view('install/error', [ + 'error' => lang('Install.messages.seedError'), + ]); + } + } + + /** + * Returns the form to create a the first superadmin user for the instance. + */ + public function createSuperAdmin() + { + helper('form'); + + return view('install/superadmin'); + } + + /** + * Creates the first superadmin user or redirects back to form if any error. + * + * After creation, user is redirected to login page to input its credentials. + */ + public function attemptCreateSuperAdmin() + { + $userModel = new UserModel(); + + // Validate here first, since some things, + // like the password, can only be validated properly here. + $rules = array_merge( + $userModel->getValidationRules(['only' => ['username']]), + [ + 'email' => 'required|valid_email|is_unique[users.email]', + 'password' => 'required|strong_password', + ] + ); + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + // Save the user + $user = new \App\Entities\User($this->request->getPost()); + + // Activate user + $user->activate(); + + $db = \Config\Database::connect(); + + $db->transStart(); + if (!($userId = $userModel->insert($user, true))) { + $db->transComplete(); + + return redirect() + ->back() + ->withInput() + ->with('errors', $userModel->errors()); + } + + // add newly created user to superadmin group + $authorization = Services::authorization(); + $authorization->addUserToGroup($userId, 'superadmin'); + + $db->transComplete(); + + // Success! + // set redirect url to admin page after being redirected to login page + $_SESSION['redirect_url'] = route_to('admin'); + + return redirect() + ->route('login') + ->with('message', lang('Install.messages.createSuperAdminSuccess')); + } +} diff --git a/app/Controllers/Migrate.php b/app/Controllers/Migrate.php deleted file mode 100644 index 6cf2b699..00000000 --- a/app/Controllers/Migrate.php +++ /dev/null @@ -1,25 +0,0 @@ -latest(); - } catch (\Exception $e) { - // Do something with the error here... - } - } -} diff --git a/app/Database/Seeds/AppSeeder.php b/app/Database/Seeds/AppSeeder.php new file mode 100644 index 00000000..6dac6af4 --- /dev/null +++ b/app/Database/Seeds/AppSeeder.php @@ -0,0 +1,25 @@ +call('AuthSeeder'); + $this->call('CategorySeeder'); + $this->call('LanguageSeeder'); + $this->call('PlatformSeeder'); + } +} diff --git a/app/Language/en/AdminNavigation.php b/app/Language/en/AdminNavigation.php index 714502a4..255e1b5e 100644 --- a/app/Language/en/AdminNavigation.php +++ b/app/Language/en/AdminNavigation.php @@ -10,7 +10,7 @@ return [ 'dashboard' => 'Dashboard', 'podcasts' => 'Podcasts', 'users' => 'Users', - 'admin_home' => 'Home', + 'admin' => 'Home', 'my_podcasts' => 'My podcasts', 'podcast_list' => 'All podcasts', 'podcast_create' => 'New podcast', diff --git a/app/Language/en/Install.php b/app/Language/en/Install.php new file mode 100644 index 00000000..ede3aa93 --- /dev/null +++ b/app/Language/en/Install.php @@ -0,0 +1,42 @@ + [ + 'castopod_config' => 'Castopod configuration', + 'hostname' => 'Hostname', + 'admin_gateway' => 'Admin gateway', + 'auth_gateway' => 'Auth gateway', + 'db_config' => 'Database configuration', + 'db_hostname' => 'Database hostname', + 'db_name' => 'Database name', + 'db_username' => 'Database username', + 'db_password' => 'Database password', + 'db_prefix' => 'Database prefix', + 'submit_install' => 'Install!', + 'create_superadmin' => 'Create your superadmin account', + 'email' => 'Email', + 'username' => 'Username', + 'password' => 'Password', + 'submit_create_superadmin' => 'Create superadmin!', + ], + 'messages' => [ + 'migrateSuccess' => + 'Database has been created successfully, and all required data have been stored!', + 'createSuperAdminSuccess' => + 'Your superadmin account has been created successfully. Let\'s login to the admin area!', + 'databaseConnectError' => + 'Unable to connect to the database. Make sure the values in .env are correct. If not, edit them and refresh the page or delete the .env file to restart install.', + 'migrationError' => + 'There was an issue during migration. Make sure the values in .env are correct. If not, edit them and refresh the page or delete the .env file to restart install.', + 'seedError' => + 'There was an issue when seeding the database. Make sure the values in .env are correct. If not, edit them and refresh the page or delete the .env file to restart install.', + 'error' => + 'An error occurred during install
{message}', + ], +]; diff --git a/app/Views/admin/_header.php b/app/Views/admin/_header.php index b97236e3..6756660b 100644 --- a/app/Views/admin/_header.php +++ b/app/Views/admin/_header.php @@ -1,7 +1,7 @@
Admin diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php index fa349177..dad00549 100644 --- a/app/Views/admin/_sidenav.php +++ b/app/Views/admin/_sidenav.php @@ -1,6 +1,6 @@ ['icon' => 'dashboard', 'items' => ['admin_home']], + 'dashboard' => ['icon' => 'dashboard', 'items' => ['admin']], 'podcasts' => [ 'icon' => 'mic', 'items' => ['my_podcasts', 'podcast_list', 'podcast_create'], diff --git a/app/Views/install/_layout.php b/app/Views/install/_layout.php new file mode 100644 index 00000000..81f634ff --- /dev/null +++ b/app/Views/install/_layout.php @@ -0,0 +1,26 @@ + + + + + + Castopod + + + + + + + +
+
+ Castopod installer +
+
+
+ + renderSection('content') ?> +
+
+ diff --git a/app/Views/install/env.php b/app/Views/install/env.php new file mode 100644 index 00000000..d41eff3f --- /dev/null +++ b/app/Views/install/env.php @@ -0,0 +1,89 @@ +extend('install/_layout') ?> + +section('content') ?> + + 'flex flex-col max-w-sm mx-auto', +]) ?> + + 'flex flex-col mb-6']) ?> + + + 'hostname', + 'name' => 'hostname', + 'class' => 'form-input mb-4', + 'value' => config('App')->baseURL, + ]) ?> + + + 'admin_gateway', + 'name' => 'admin_gateway', + 'class' => 'form-input mb-4', + 'value' => config('App')->adminGateway, + ]) ?> + + + 'auth_gateway', + 'name' => 'auth_gateway', + 'class' => 'form-input', + 'value' => config('App')->authGateway, + ]) ?> + + + 'flex flex-col mb-6']) ?> + + + 'db_hostname', + 'name' => 'db_hostname', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['hostname'], + ]) ?> + + + 'db_name', + 'name' => 'db_name', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['database'], + ]) ?> + + + 'db_username', + 'name' => 'db_username', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['username'], + ]) ?> + + + 'db_password', + 'name' => 'db_password', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['password'], + ]) ?> + + + 'db_prefix', + 'name' => 'db_prefix', + 'class' => 'form-input', + 'value' => config('Database')->default['DBPrefix'], + ]) ?> + + + lang('Install.form.submit_install'), + 'type' => 'submit', + 'class' => 'self-end px-4 py-2 bg-gray-200', +]) ?> + + + +endSection() ?> diff --git a/app/Views/install/error.php b/app/Views/install/error.php new file mode 100644 index 00000000..b4f8bf0a --- /dev/null +++ b/app/Views/install/error.php @@ -0,0 +1,9 @@ +extend('install/_layout') ?> + +section('content') ?> + +
+ $error]) ?> +
+ +endSection() ?> diff --git a/app/Views/install/superadmin.php b/app/Views/install/superadmin.php new file mode 100644 index 00000000..37858097 --- /dev/null +++ b/app/Views/install/superadmin.php @@ -0,0 +1,45 @@ +extend('install/_layout') ?> + +section('content') ?> + + 'flex flex-col max-w-sm mx-auto', +]) ?> + + 'flex flex-col mb-6']) ?> + + + 'email', + 'name' => 'email', + 'class' => 'form-input mb-4', + 'type' => 'email', + ]) ?> + + + 'username', + 'name' => 'username', + 'class' => 'form-input mb-4', + ]) ?> + + + 'password', + 'name' => 'password', + 'class' => 'form-input', + 'type' => 'password', + ]) ?> + + + lang('Install.form.submit_create_superadmin'), + 'type' => 'submit', + 'class' => 'self-end px-4 py-2 bg-gray-200', +]) ?> + + + +endSection() ?> diff --git a/composer.json b/composer.json index c5f29a4a..44be1c8c 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "geoip2/geoip2": "~2.0", "myth/auth": "dev-develop", "codeigniter4/codeigniter4": "dev-develop", - "league/commonmark": "^1.5" + "league/commonmark": "^1.5", + "vlucas/phpdotenv": "^5.1" }, "require-dev": { "mikey179/vfsstream": "1.6.*", diff --git a/composer.lock b/composer.lock index 1d5b5b7c..bdeba108 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1fe52c47fa9834960fdb1cf37f2f2776", + "content-hash": "a6be291e1c7f73b73182cd7b49234688", "packages": [ { "name": "codeigniter4/codeigniter4", @@ -186,6 +186,68 @@ ], "time": "2019-12-12T18:48:39+00:00" }, + { + "name": "graham-campbell/result-type", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/7e279d2cd5d7fbb156ce46daada972355cea27bb", + "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0", + "phpoption/phpoption": "^1.7.3" + }, + "require-dev": { + "phpunit/phpunit": "^6.5|^7.5|^8.5|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2020-04-13T13:17:36+00:00" + }, { "name": "james-heinrich/getid3", "version": "v2.0.0-beta3", @@ -705,6 +767,71 @@ ], "time": "2020-07-16T14:00:14+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-07-20T17:29:33+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -798,6 +925,315 @@ ], "time": "2020-03-23T09:12:05+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-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" + ], + "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": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-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" + ], + "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": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "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": "2020-07-14T12:35:20+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "448c76d7a9e30c341ff5bc367a923af74ae18467" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/448c76d7a9e30c341ff5bc367a923af74ae18467", + "reference": "448c76d7a9e30c341ff5bc367a923af74ae18467", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.1", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.7.4", + "symfony/polyfill-ctype": "^1.17", + "symfony/polyfill-mbstring": "^1.17", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.2 || ^9.0" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "homepage": "https://gjcampbell.co.uk/" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://vancelucas.com/" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2020-07-14T19:26:25+00:00" + }, { "name": "whichbrowser/parser", "version": "v2.0.42", @@ -2351,82 +2787,6 @@ ], "time": "2020-04-17T01:09:41+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-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" - ], - "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": "2020-07-14T12:35:20+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.0", diff --git a/public/favicon.ico b/public/favicon.ico index aa74302f6ff59e0fb3327fc9511f4116636bb5cb..3a7011d31145b36525ab60c8ed7dc0dbf07d7303 100644 GIT binary patch literal 3758 zcmeHIO-NKx6#kxX9Lph?!YsmM5*J1UM)V^|j3rhJMTq_u$+Rimi2ZC@jLgC$f+Dbp zfr^3_K`{&k5mu7sqA`6~SVN?eW`EtMMMgTsap?}H)n%L&|8+vmgQ0D?F*vBjUc!>Ar!n)WdR1a*%&Y?Q& z9o~l{BZqPPRU0}+I}m;w#*K-axSzO({^>qUew_k+)A9Z1k6FVlHmzsO=UE>M`%ib4 z#7<$w=-_~Qd+7R=LxL=R9fP~5CVynlNVkEG7 zoTyD0S`)j=%2okOHbg$Ks*Hz(RbWAqD=lB=$$+9YvM~V^_Ijcv*MLaK)Sr8zD^1<4 z8_ql{T0q2;s|cfvhxWv_GKuoYM`4xw5qmQCgI$kAiHwcCxgh8qGG;*3xt~Xf$Ny00 ze2g6aBm*shj9xC9%E-G({(N)#>@FQv~TKS@86NShdInLE*B?~&Wb5I`M`loHTL2;A(w$%267qrHyQW^ DI;5X| literal 3758 zcmb_eU1%It6#kNIGf9&sYen!W*+!8_wNyc&(4VD5#Rs)gUqmdWzDPkxslL>oE7{qgm{-34HhK1K24BXRBMfm^S~BiqJs zU}_7VerzvZeC9A-pM3%EojQry`*T?O>|@l=eT|h%7qRx|A1!GX7i+-qqB0e%hgk6x z4Z;tWFpO>0GXvG*z}uUO=skkM$->lBd3{;kDi9egDyurgaOB`&b%@~2(_&|a2)=J#g;*c{#_C~&51ij4(g^i?IrtB7db#?t z>cLXIUjGikNA=}ti~!GmzI$RqHtEKhbByR++Rs&~f4Kzo{}5?pr}qPcbQ*kjXnyl7 zL-}O&;Ct`?+H`9^*1PQ*rE8^Lh=A=x#C}5H;SYK2NMF2S`}g@|07*>-l+~HQlT6%e zcHp5m2%L0K)oq^?OBtL>qolA{b9J=Ytn@Vh2990@cC7*9tH3C$1T~;fo(hNS9&{n+ zC%hKLhs-58CAZX&T2fPL%Q-LesqpzP=e*l@!JYMHlGqH}Nm`P!fR>WhSazWB*yx2M zoa2y&TO6`*#z7lT`y^i@2Tr|bI?`!z@44ZL1TG-UqXccj;{<7ZrV^)#ULn>+D41Wo zt_9NJ>&#`3Yl1XhFe@3{t0MK9M#)=PARP`Ym`X={pSk35Hjs@UGUmoE`<)5>`T;HLsFr%ur zsd&FJSDw6mLP(Qh;|=#L7$XT3-(eb>%z}1h$+HfhGoL0QEAP-jk5VZZQ*On*b-?`A z3IiwHpoh&*dH7CKv*t1Ll?vOr^N)TD9JAeQa38!aW?u0*wsE1scI*ilG8b+9&KTdC zi;qQxTJryB<2Q8aZ!mczZHTPL-1`+wW2-kisn1Vs!F=MZp5J1wP|^Ll-~3+ofRzeg znx7pW7_+i>ll^nD>&83gjG1;gTgf5)9cg|9@%i$84GHJRQdZpT?M`|_0e8M{de#2- zx%JEeh54aewL<=y^ZQ9TcEjZZkqXEtf_G?u>AT|{3P^Z|1e(4(-nM{E-!?&%o?e-^ zj#L-kCV>ynbx6W_6?}G;vMprb>@K9Uw`9mhXB={28g;8%Qip;1%MC;XI;l&Iw-nK( zb27r}z{?7+E6rvooRKx*bgT&H-9;D!c$X16>bQp6dz9G6H4S-m`bI;svT~>lf#WqO z$$d*m?p#80?-FXuE&RW)ZR=HRDY@GT#r=k=BFJ$2UE%w>IXLd-