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
This commit is contained in:
Yassine Doghri 2020-08-12 20:03:45 +00:00
parent 14dd44d03d
commit cba871c5df
18 changed files with 965 additions and 112 deletions

View File

@ -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:

View File

@ -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';
}

View File

@ -38,7 +38,7 @@ class Database extends \CodeIgniter\Database\Config
'password' => '',
'database' => '',
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'DBPrefix' => 'cp_',
'pConnect' => false,
'DBDebug' => ENVIRONMENT !== 'production',
'cacheOn' => false,

View File

@ -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', [

270
app/Controllers/Install.php Normal file
View File

@ -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'));
}
}

View File

@ -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...
}
}
}

View File

@ -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');
}
}

View File

@ -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',

View File

@ -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}',
],
];

View File

@ -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

View File

@ -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'],

View File

@ -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>

89
app/Views/install/env.php Normal file
View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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.*",

514
composer.lock generated
View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB