From cba871c5df9f7120c44d9952456ebbd0d220669e Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> 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 62ea05ab40..612c3284a3 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 55952114ae..f05de3d87e 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 a77c5865d3..4085367592 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 f65494dde7..dc8d3ce95b 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 0000000000..fd5f4f6835 --- /dev/null +++ b/app/Controllers/Install.php @@ -0,0 +1,270 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Controllers; + +use App\Models\UserModel; +use Config\Services; +use Dotenv\Dotenv; +use Exception; + +class Install extends BaseController +{ + /** + * Every operation goes through this method to handle + * the install logic. + * + * If all required actions have already been performed, + * the install route will show a 404 page. + */ + public function index() + { + try { + // Check if .env is created and has all required fields + $dotenv = Dotenv::createImmutable(ROOTPATH); + + $dotenv->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 6cf2b69923..0000000000 --- a/app/Controllers/Migrate.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Controllers; - -use CodeIgniter\Controller; - -class Migrate extends Controller -{ - public function index() - { - $migrate = \Config\Services::migrations(); - - try { - $migrate->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 0000000000..6dac6af4d6 --- /dev/null +++ b/app/Database/Seeds/AppSeeder.php @@ -0,0 +1,25 @@ +<?php + +/** + * Class AppSeeder + * Calls all required seeders for castopod to work properly + * + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Seeds; + +use CodeIgniter\Database\Seeder; + +class AppSeeder extends Seeder +{ + public function run() + { + $this->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 714502a495..255e1b5ed0 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 0000000000..ede3aa9391 --- /dev/null +++ b/app/Language/en/Install.php @@ -0,0 +1,42 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'form' => [ + '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' => + '<strong>An error occurred during install</strong><br/> {message}', + ], +]; diff --git a/app/Views/admin/_header.php b/app/Views/admin/_header.php index b97236e3cf..6756660b62 100644 --- a/app/Views/admin/_header.php +++ b/app/Views/admin/_header.php @@ -1,7 +1,7 @@ <header class="<?= $class ?>"> <div class="w-64"> <a href="<?= route_to( - 'admin_home' + 'admin' ) ?>" class="inline-flex items-center text-xl"> <?= svg('logo-castopod', 'text-3xl mr-2') ?> Admin diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php index fa349177e1..dad0054913 100644 --- a/app/Views/admin/_sidenav.php +++ b/app/Views/admin/_sidenav.php @@ -1,6 +1,6 @@ <?php $navigation = [ - 'dashboard' => ['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 0000000000..81f634ff01 --- /dev/null +++ b/app/Views/install/_layout.php @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"/> + <title>Castopod</title> + <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> + <link rel="stylesheet" href="/assets/index.css"/> +</head> + +<body class="flex flex-col min-h-screen mx-auto"> + <header class="border-b"> + <div class="container flex items-center justify-between px-2 py-4 mx-auto"> + Castopod installer + </div> + </header> + <main class="container flex-1 px-4 py-10 mx-auto"> + <?= view('_message_block') ?> + <?= $this->renderSection('content') ?> + </main> + <footer class="container px-2 py-4 mx-auto text-sm text-right border-t"> + Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative. + </footer> +</body> diff --git a/app/Views/install/env.php b/app/Views/install/env.php new file mode 100644 index 0000000000..d41eff3fb5 --- /dev/null +++ b/app/Views/install/env.php @@ -0,0 +1,89 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<?= form_open(route_to('install_generate_env'), [ + 'class' => 'flex flex-col max-w-sm mx-auto', +]) ?> + +<?= form_fieldset('', ['class' => 'flex flex-col mb-6']) ?> + <legend class="mb-4 text-xl"><?= lang( + 'Install.form.castopod_config' + ) ?></legend> + <?= form_label(lang('Install.form.hostname'), 'hostname') ?> + <?= form_input([ + 'id' => 'hostname', + 'name' => 'hostname', + 'class' => 'form-input mb-4', + 'value' => config('App')->baseURL, + ]) ?> + + <?= form_label(lang('Install.form.admin_gateway'), 'admin_gateway') ?> + <?= form_input([ + 'id' => 'admin_gateway', + 'name' => 'admin_gateway', + 'class' => 'form-input mb-4', + 'value' => config('App')->adminGateway, + ]) ?> + + <?= form_label(lang('Install.form.auth_gateway'), 'auth_gateway') ?> + <?= form_input([ + 'id' => 'auth_gateway', + 'name' => 'auth_gateway', + 'class' => 'form-input', + 'value' => config('App')->authGateway, + ]) ?> +<?= form_fieldset_close() ?> + +<?= form_fieldset('', ['class' => 'flex flex-col mb-6']) ?> + <legend class="mb-4 text-xl"><?= lang('Install.form.db_config') ?></legend> + <?= form_label(lang('Install.form.db_hostname'), 'db_hostname') ?> + <?= form_input([ + 'id' => 'db_hostname', + 'name' => 'db_hostname', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['hostname'], + ]) ?> + + <?= form_label(lang('Install.form.db_name'), 'db_name') ?> + <?= form_input([ + 'id' => 'db_name', + 'name' => 'db_name', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['database'], + ]) ?> + + <?= form_label(lang('Install.form.db_username'), 'db_username') ?> + <?= form_input([ + 'id' => 'db_username', + 'name' => 'db_username', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['username'], + ]) ?> + + <?= form_label(lang('Install.form.db_password'), 'db_password') ?> + <?= form_input([ + 'id' => 'db_password', + 'name' => 'db_password', + 'class' => 'form-input mb-4', + 'value' => config('Database')->default['password'], + ]) ?> + + <?= form_label(lang('Install.form.db_prefix'), 'db_prefix') ?> + <?= form_input([ + 'id' => 'db_prefix', + 'name' => 'db_prefix', + 'class' => 'form-input', + 'value' => config('Database')->default['DBPrefix'], + ]) ?> +<?= form_fieldset_close() ?> + +<?= form_button([ + 'content' => lang('Install.form.submit_install'), + 'type' => 'submit', + 'class' => 'self-end px-4 py-2 bg-gray-200', +]) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/app/Views/install/error.php b/app/Views/install/error.php new file mode 100644 index 0000000000..b4f8bf0ac0 --- /dev/null +++ b/app/Views/install/error.php @@ -0,0 +1,9 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<div class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700"> + <?= lang('Install.messages.error', ['message' => $error]) ?> +</div> + +<?= $this->endSection() ?> diff --git a/app/Views/install/superadmin.php b/app/Views/install/superadmin.php new file mode 100644 index 0000000000..37858097ae --- /dev/null +++ b/app/Views/install/superadmin.php @@ -0,0 +1,45 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<?= form_open(route_to('install_create_superadmin'), [ + 'class' => 'flex flex-col max-w-sm mx-auto', +]) ?> + +<?= form_fieldset('', ['class' => 'flex flex-col mb-6']) ?> + <legend class="mb-4 text-xl"><?= lang( + 'Install.form.create_superadmin' + ) ?></legend> + <?= form_label(lang('Install.form.email'), 'email') ?> + <?= form_input([ + 'id' => 'email', + 'name' => 'email', + 'class' => 'form-input mb-4', + 'type' => 'email', + ]) ?> + + <?= form_label(lang('Install.form.username'), 'username') ?> + <?= form_input([ + 'id' => 'username', + 'name' => 'username', + 'class' => 'form-input mb-4', + ]) ?> + + <?= form_label(lang('Install.form.password'), 'password') ?> + <?= form_input([ + 'id' => 'password', + 'name' => 'password', + 'class' => 'form-input', + 'type' => 'password', + ]) ?> +<?= form_fieldset_close() ?> + +<?= form_button([ + 'content' => lang('Install.form.submit_create_superadmin'), + 'type' => 'submit', + 'class' => 'self-end px-4 py-2 bg-gray-200', +]) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/composer.json b/composer.json index c5f29a4aef..44be1c8c65 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 1d5b5b7c85..bdeba108ec 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`E<GvuP1TQRm+GGxJhsS|mZt2lu{n&UfCq_nvp& zxqt!{Uayy*E>tMMMgTsap?}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~5CV<OvR9AI9L9knxfm^|gdl(1e1w&(u(Ke{Hj7o!@ zhgsyw$HUL#Z{_Lbmw47(@pAKmH6dr%hNx37edp8*b-!h(dN?@vG*3%D>ynlNVkEG7 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)#<kROlIYFo-nnMO?6v)5=?O8ThLUe3q0JS+R5rB*P zMR&Nf(&1CAi8jLE!#7Dmm-mwCr1nS~Pu%HDWjZZFy+u<Wzp*qT)e(CIl_=RMuC3bb z^6W}^Z3}l%I{<I_sO@YD%ZYj|TRXkC#MHCN{t$}Q08=DO1$LtIDb<QoE{aBthPF_g zY9?CgykYc^aYt;41NHqt@Q$=^rKrI#q6sNn<r9tT{DqAQD5b|)_+f+h(4-VZ{DYje z;9aTO@w4sEP19Z@Qu?g)ScQ~FmXyxBiF~GhZnlzYu=9(xMCsKa-zp}0VTI$8(od{+ zV24KJw|KLp>>@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>oE<OlK zD1qveSc{1Er6DRPWi_n&pkN|Om8#8_vHrBC?j;C99;)ki&Ye4VW_EU0!~=8B&v*Xj z+`F?E=s_Qf#UgnDlRdzl07@mRU+x9=(N->7{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{;<UF3L+-S!*={Yb|laU_iwzNjjE9jU;& zYvVu|0JT1#!a2y(jvOw&5+kuPrxKqspMN{2Ll5oNS<N1KqT{TtHveK)OGZg#E6EyK zXe6>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@UGUm<Lfw+~gdrAIA zs?}-qUNGi-_0)sb9^sQTD)HsFtI&8}9c=n_4mQ8?4Nh2xJ>oE`<)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-<mTqMn`=Dh8c3YG dCgwh90`803C%11Bz-{sSN{3ydH$&O?e*ly$JfZ*q -- GitLab