diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..a1b2a681038cc172a9d68b9cfe873d606149614b --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +#-------------------------------------------------------------------- +# Example Environment Configuration file +# +# This file can be used as a starting point for +# your castopod instance settings. +# +# For manual configuration: +# - copy this file's contents to a file named `.env` +# - replace all the default settings with your values +# - go to `/cp-install` to complete installation +#-------------------------------------------------------------------- + +# Instance configuration +app.baseURL="https://YOUR_DOMAIN_NAME/" +app.adminGateway="cp-admin" +app.authGateway="cp-auth" + +# Database configuration +database.default.hostname="localhost" +database.default.database="castopod" +database.default.username="root" +database.default.password="root" +database.default.DBPrefix="cp_" + +# Cache configuration (advanced) +# Keep as is if you don't know what this means +cache.handler="file" diff --git a/.rsync-filter b/.rsync-filter index 1ee766c269e244c790f1f3e3c4accf3d130a729b..606c8a9106df37b3e6caa7f88a52db6d4acf094e 100644 --- a/.rsync-filter +++ b/.rsync-filter @@ -5,7 +5,9 @@ + public/*** + vendor/*** + writable/*** ++ .env.example + DEPENDENCIES.md + LICENSE + README.md ++ INSTALL.md - ** diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000000000000000000000000000000000000..3f07f1aadaa36c72b185c0b90aa0d7099d2b808a --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,73 @@ +# How to install Castopod + +Castopod was thought to be easy to install. Whether using dedicated or shared hosting, you can install it on most PHP-MySQL compatible web servers. + +- [Install instructions](#install-instructions) + - [(optional) Manual configuration](#optional-manual-configuration) +- [Web Server Requirements](#web-server-requirements) + - [PHP v7.2 or higher](#php-v72-or-higher) + - [MySQL compatible database](#mysql-compatible-database) + - [(Optional) Other recommendations](#optional-other-recommendations) +- [Security concerns](#security-concerns) + +## Install instructions + +0. Create a MySQL database for Castopod with a user having access and modification privileges (for more info, see [Web Server Requirements](#web-server-requirements)). +1. Download and unzip the Castopod package onto the web server if you haven’t already. + - âš ï¸ Set the web server document root to the `public/` sub-folder. +2. Run the Castopod install script by going to the install wizard page (`https://your_domain_name.com/cp-install`) in your favorite web browser. +3. Follow the instructions on your screen. + +All done, start podcasting! + +### (optional) Manual configuration + +Before uploading Castopod files to your web server: + +1. Rename the `.env.example` file to `.env` and update the default values with your own. +2. Upload the Castopod files with `.env` +3. Go to `/cp-install` to finish the install process. + +## Web Server Requirements + +### PHP v7.2 or higher + +PHP version 7.2 or higher is required, with the following extensions installed: + +- [intl](http://php.net/manual/en/intl.requirements.php) +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library +- [mbstring](http://php.net/manual/en/mbstring.installation.php) + +Additionally, make sure that the following extensions are enabled in your PHP: + +- json (enabled by default - don't turn it off) +- xml (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) + +### MySQL compatible database + +> We recommend using [MariaDB](https://mariadb.org) + +You will need the server hostname, database name, username and password to complete the installation process. If you do not have these, please contact your server administrator. + +#### Privileges + +User must have at least these privileges on the database for Castopod to work: `ALTER`, `DELETE`, `EXECUTE`, `INDEX`, `INSERT`, `SELECT`, `UPDATE`. + +### (Optional) Other recommendations + +- Redis for better cache performances. +- CDN for better performances. +- e-mail gateway for lost passwords. + +## Security concerns + +Castopod is built on top of Codeigniter, a PHP framework that encourages [good security practices](https://codeigniter.com/user_guide/concepts/security.html). + +To maximize your instance safety and prevent any malicious attack, we recommend you update all your Castopod files permissions: + +- `writable/` folder must be **readable** and **writable**. +- `public/media/` folder must be **readable** and **writable**. +- any other file must be set to **readonly**. + +// TODO: add instructions on how to set file permissions. diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 159afc4764cdd67b87fae28ec034ced162b1e3fd..b580e3f4edb79e8de24c4a7e9cde794c61b4ecc5 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -45,8 +45,14 @@ $routes->get('/', 'Home::index', ['as' => 'home']); // Install Wizard route $routes->group(config('App')->installGateway, function ($routes) { $routes->get('/', 'Install', ['as' => 'install']); - $routes->post('generate-env', 'Install::attemptCreateEnv', [ - 'as' => 'generate-env', + $routes->post('instance-config', 'Install::attemptInstanceConfig', [ + 'as' => 'instance-config', + ]); + $routes->post('database-config', 'Install::attemptDatabaseConfig', [ + 'as' => 'database-config', + ]); + $routes->post('cache-config', 'Install::attemptCacheConfig', [ + 'as' => 'cache-config', ]); $routes->post('create-superadmin', 'Install::attemptCreateSuperAdmin', [ 'as' => 'create-superadmin', diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 64594e07430fcf56196e43fe652a6bea67f2aaf4..1e68a844372f24477e2c6a78e477ce4a13cada87 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -176,7 +176,7 @@ class Podcast extends BaseController helper(['media', 'misc']); $rules = [ - 'imported_feed_url' => 'required|valid_url', + 'imported_feed_url' => 'required|validate_url', 'season_number' => 'is_natural_no_zero|permit_empty', 'max_episodes' => 'is_natural_no_zero|permit_empty', ]; diff --git a/app/Controllers/Admin/PodcastSettings.php b/app/Controllers/Admin/PodcastSettings.php index f4b65a893596064dad4d7d39676786c3d2b41df8..e9c079c92ae372cd1d15caea0f5942b0b4477e90 100644 --- a/app/Controllers/Admin/PodcastSettings.php +++ b/app/Controllers/Admin/PodcastSettings.php @@ -64,7 +64,7 @@ class PodcastSettings extends BaseController $platformLinkUrl = $platformLink['url']; if ( !empty($platformLinkUrl) && - $validation->check($platformLinkUrl, 'valid_url') + $validation->check($platformLinkUrl, 'validate_url') ) { $platformId = $platformModel->getPlatformId($platformName); array_push($platformLinksData, [ diff --git a/app/Controllers/Install.php b/app/Controllers/Install.php index fd5f4f6835369a222e9dc3f5ae02a4bdad1a1e1d..712033c22c7ea7cce0437df2ad5f15846213cd9a 100644 --- a/app/Controllers/Install.php +++ b/app/Controllers/Install.php @@ -9,12 +9,26 @@ namespace App\Controllers; use App\Models\UserModel; +use CodeIgniter\Controller; use Config\Services; use Dotenv\Dotenv; -use Exception; -class Install extends BaseController +class Install extends Controller { + protected $helpers = ['form', 'components', 'svg']; + + /** + * Constructor. + */ + public function initController( + \CodeIgniter\HTTP\RequestInterface $request, + \CodeIgniter\HTTP\ResponseInterface $response, + \Psr\Log\LoggerInterface $logger + ) { + // Do Not Edit This Line + parent::initController($request, $response, $logger); + } + /** * Every operation goes through this method to handle * the install logic. @@ -26,25 +40,61 @@ class Install extends BaseController { try { // Check if .env is created and has all required fields - $dotenv = Dotenv::createImmutable(ROOTPATH); + $dotenv = Dotenv::createUnsafeImmutable(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 + // Check if the created .env file is writable to continue install process + if (is_writable(ROOTPATH . '.env')) { + try { + $dotenv->required([ + 'app.baseURL', + 'app.adminGateway', + 'app.authGateway', + ]); + } catch (\Dotenv\Exception\ValidationException $e) { + // form to input instance configuration + return $this->instanceConfig(); + } + + try { + $dotenv->required([ + 'database.default.hostname', + 'database.default.database', + 'database.default.username', + 'database.default.password', + 'database.default.DBPrefix', + ]); + } catch (\Dotenv\Exception\ValidationException $e) { + return $this->databaseConfig(); + } + + try { + $dotenv->required('cache.handler'); + } catch (\Dotenv\Exception\ValidationException $e) { + return $this->cacheConfig(); + } + } else { + try { + $dotenv->required([ + 'app.baseURL', + 'app.adminGateway', + 'app.authGateway', + 'database.default.hostname', + 'database.default.database', + 'database.default.username', + 'database.default.password', + 'database.default.DBPrefix', + 'cache.handler', + ]); + } catch (\Dotenv\Exception\ValidationException $e) { + return view('install/manual_config'); + } + } + try { $db = db_connect(); @@ -57,10 +107,14 @@ class Install extends BaseController 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'), - ]); + // Could not connect to the database + // show database config view to fix value + session()->setFlashdata( + 'error', + lang('Install.messages.databaseConnectError') + ); + + return view('install/database_config'); } // migrate if no user has been created @@ -77,92 +131,108 @@ class Install extends BaseController */ public function createEnv() { - helper('form'); + // create empty .env file + try { + $envFile = fopen(ROOTPATH . '.env', 'w'); + fclose($envFile); + } catch (\Throwable $e) { + // Could not create the .env file, redirect to a view with manual instructions on how to add it + return view('install/manual_config'); + } - return view('install/env'); + return redirect()->back(); } - /** - * Verifies that all fields have been submitted correctly and - * creates the .env file after user submits the install form. - */ - public function attemptCreateEnv() + public function instanceConfig() { - 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 view('install/instance_config'); + } + + public function attemptInstanceConfig() + { + $rules = [ + 'hostname' => 'required|validate_url', + 'admin_gateway' => 'required', + 'auth_gateway' => 'required|differs[admin_gateway]', + ]; + + if (!$this->validate($rules)) { return redirect() ->back() + ->withInput() ->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.'); - } + self::writeEnv([ + 'app.baseURL' => $this->request->getPost('hostname'), + 'app.adminGateway' => $this->request->getPost('admin_gateway'), + 'app.authGateway' => $this->request->getPost('auth_gateway'), + ]); - $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(); + } - return redirect()->back(); - } catch (\Throwable $e) { + public function databaseConfig() + { + return view('install/database_config'); + } + + public function attemptDatabaseConfig() + { + $rules = [ + 'db_hostname' => 'required', + 'db_name' => 'required', + 'db_username' => 'required', + 'db_password' => 'required', + ]; + + if (!$this->validate($rules)) { return redirect() ->back() - ->with('error', $e->getMessage()); - } finally { - fclose($envFile); + ->withInput() + ->with('errors', $this->validator->getErrors()); } + + self::writeEnv([ + 'database.default.hostname' => $this->request->getPost( + 'db_hostname' + ), + 'database.default.database' => $this->request->getPost('db_name'), + 'database.default.username' => $this->request->getPost( + 'db_username' + ), + 'database.default.password' => $this->request->getPost( + 'db_password' + ), + 'database.default.DBPrefix' => $this->request->getPost('db_prefix'), + ]); + + return redirect()->back(); + } + + public function cacheConfig() + { + return view('install/cache_config'); + } + + public function attemptCacheConfig() + { + $rules = [ + 'cache_handler' => 'required', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + self::writeEnv([ + 'cache.handler' => $this->request->getPost('cache_handler'), + ]); + + return redirect()->back(); } /** @@ -172,14 +242,8 @@ class Install extends BaseController { $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'), - ]); - } + !$migrations->setNamespace('Myth\Auth')->latest(); + !$migrations->setNamespace(APP_NAMESPACE)->latest(); } /** @@ -187,16 +251,10 @@ class Install extends BaseController */ public function seed() { - try { - $seeder = \Config\Database::seeder(); + $seeder = \Config\Database::seeder(); - // Seed database - $seeder->call('AppSeeder'); - } catch (\Throwable $e) { - return view('install/error', [ - 'error' => lang('Install.messages.seedError'), - ]); - } + // Seed database + $seeder->call('AppSeeder'); } /** @@ -204,9 +262,7 @@ class Install extends BaseController */ public function createSuperAdmin() { - helper('form'); - - return view('install/superadmin'); + return view('install/create_superadmin'); } /** @@ -245,7 +301,7 @@ class Install extends BaseController $db->transStart(); if (!($userId = $userModel->insert($user, true))) { - $db->transComplete(); + $db->transRollback(); return redirect() ->back() @@ -260,11 +316,47 @@ class Install extends BaseController $db->transComplete(); // Success! - // set redirect url to admin page after being redirected to login page - $_SESSION['redirect_url'] = route_to('admin'); + // set redirect_url session as admin area to go to after login + session()->set('redirect_url', route_to('admin')); return redirect() ->route('login') ->with('message', lang('Install.messages.createSuperAdminSuccess')); } + + /** + * writes config values in .env file + * overwrites any existing key and appends new ones + * + * @param array $data key/value config pairs + * + * @return void + */ + public static function writeEnv($configData) + { + $envData = file(ROOTPATH . '.env'); // reads an array of lines + + foreach ($configData as $key => $value) { + $replaced = false; + $keyVal = $key . '="' . $value . '"' . PHP_EOL; + $envData = array_map(function ($line) use ( + $key, + $keyVal, + &$replaced + ) { + if (strpos($line, $key) === 0) { + $replaced = true; + return $keyVal; + } + return $line; + }, + $envData); + + if (!$replaced) { + array_push($envData, $keyVal); + } + } + + file_put_contents(ROOTPATH . '.env', implode('', $envData)); + } } diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index c07833b31ee50ba5abc64e629eddadc3ec89d132..8c509669f5968f1b1ee7e57c2c11cf544af714c3 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -247,10 +247,17 @@ class AuthSeeder extends Seeder } } - $this->db->table('auth_permissions')->insertBatch($dataPermissions); - $this->db->table('auth_groups')->insertBatch($dataGroups); + $this->db + ->table('auth_permissions') + ->ignore(true) + ->insertBatch($dataPermissions); + $this->db + ->table('auth_groups') + ->ignore(true) + ->insertBatch($dataGroups); $this->db ->table('auth_groups_permissions') + ->ignore(true) ->insertBatch($dataGroupsPermissions); } } diff --git a/app/Database/Seeds/CategorySeeder.php b/app/Database/Seeds/CategorySeeder.php index a62113981ac5c89ba004dab8327cc466131cb06a..f1c33c136b096c23a192f26bbb501b68db966666 100644 --- a/app/Database/Seeds/CategorySeeder.php +++ b/app/Database/Seeds/CategorySeeder.php @@ -797,6 +797,9 @@ class CategorySeeder extends Seeder ], ]; - $this->db->table('categories')->insertBatch($data); + $this->db + ->table('categories') + ->ignore(true) + ->insertBatch($data); } } diff --git a/app/Database/Seeds/LanguageSeeder.php b/app/Database/Seeds/LanguageSeeder.php index da497e5e30baaaeb737c74fa340241f4c1e52620..31fee8fc7c8b9d028fcb9f07054fe6be47db827a 100644 --- a/app/Database/Seeds/LanguageSeeder.php +++ b/app/Database/Seeds/LanguageSeeder.php @@ -633,6 +633,9 @@ class LanguageSeeder extends Seeder ['code' => 'zu', 'name' => 'Zulu', 'native_name' => 'isiZulu'], ]; - $this->db->table('languages')->insertBatch($data); + $this->db + ->table('languages') + ->ignore(true) + ->insertBatch($data); } } diff --git a/app/Database/Seeds/PlatformSeeder.php b/app/Database/Seeds/PlatformSeeder.php index 11e6b96b1d9452ea5a771adb024063c3ed4748b7..274e1e609003a137ad547e36d6634d2564f24568 100644 --- a/app/Database/Seeds/PlatformSeeder.php +++ b/app/Database/Seeds/PlatformSeeder.php @@ -163,6 +163,9 @@ class PlatformSeeder extends Seeder 'icon_filename' => 'tunein.svg', ], ]; - $this->db->table('platforms')->insertBatch($data); + $this->db + ->table('platforms') + ->ignore(true) + ->insertBatch($data); } } diff --git a/app/Helpers/url_helper.php b/app/Helpers/url_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..7f9e157bad1bc45de966101b25948ca42b42ff07 --- /dev/null +++ b/app/Helpers/url_helper.php @@ -0,0 +1,22 @@ +<?php + +if (!function_exists('host_url')) { + /** + * Return the host URL to use in views + * + * @return string|false + */ + function host_url() + { + if (isset($_SERVER['host'])) { + $protocol = + (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || + $_SERVER['SERVER_PORT'] == 443 + ? 'https://' + : 'http://'; + return $protocol + $_SERVER['host']; + } + + return false; + } +} diff --git a/app/Language/en/Install.php b/app/Language/en/Install.php index ede3aa9391fce57f453c911449af1915cf0e6b82..10ae23a3bb19afcbef4e16c8bcf032e2917cae61 100644 --- a/app/Language/en/Install.php +++ b/app/Language/en/Install.php @@ -7,18 +7,39 @@ */ return [ + 'manual_config' => 'Manual configuration', + 'manual_config_subtitle' => + 'Create a `.env` file with your settings and refresh the page to continue installation.', 'form' => [ - 'castopod_config' => 'Castopod configuration', + 'instance_config' => 'Instance configuration', 'hostname' => 'Hostname', 'admin_gateway' => 'Admin gateway', + 'admin_gateway_hint' => + 'The route to access the admin area (eg. https://example.com/cp-admin). It is set by default as cp-admin, we recommend you change it for security reasons.', 'auth_gateway' => 'Auth gateway', - 'db_config' => 'Database configuration', + 'auth_gateway_hint' => + 'The route to access the authentication pages (eg. https://example.com/cp-auth). It is set by default as cp-auth, we recommend you change it for security reasons.', + 'database_config' => 'Database configuration', + 'database_config_hint' => + 'Castopod needs to connect to your MySQL (or MariaDB) database. If you do not have these required info, please contact your server administrator.', 'db_hostname' => 'Database hostname', 'db_name' => 'Database name', 'db_username' => 'Database username', 'db_password' => 'Database password', 'db_prefix' => 'Database prefix', - 'submit_install' => 'Install!', + 'db_prefix_hint' => + 'The prefix of the Castopod table names, leave as is if you don\'t know what it means.', + 'cache_config' => 'Cache configuration', + 'cache_config_hint' => + 'Choose your preferred cache handler. Leave it as the default value if you have no clue what it means.', + 'cache_handler' => 'Cache handler', + 'cacheHandlerOptions' => [ + 'file' => 'File', + 'redis' => 'Redis', + 'memcached' => 'Memcached', + ], + 'next' => 'Next', + 'submit' => 'Finish install', 'create_superadmin' => 'Create your superadmin account', 'email' => 'Email', 'username' => 'Username', @@ -26,17 +47,11 @@ return [ '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!', + 'Your superadmin account has been created successfully. Login to start podcasting!', '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}', + 'Castopod could not connect to your database. Edit your database configuration and try again.', + 'writeError' => + 'Couldn\'t create/write the `.env` file. You must create it manually by following the `.env.example` file template in the Castopod package.', ], ]; diff --git a/app/Language/en/Validation.php b/app/Language/en/Validation.php index f987763075446595d6043faecf98844a62105a89..4fb156366d90225eefe3c290c792783dbfe4b07d 100644 --- a/app/Language/en/Validation.php +++ b/app/Language/en/Validation.php @@ -13,4 +13,6 @@ return [ '{field} is either not an image, or it is not wide or tall enough.', 'is_image_squared' => '{field} is either not an image, or it is not squared (width and height differ).', + 'validate_url' => + 'The {field} field must be a valid URL (eg. https://example.com/).', ]; diff --git a/app/Validation/Rules.php b/app/Validation/Rules.php index 4e98e01f5ba085f61f25b97cac13f4b4f0c19c9a..9b8ce5a4054512a163e2cc280709f35d822ba7f7 100644 --- a/app/Validation/Rules.php +++ b/app/Validation/Rules.php @@ -29,4 +29,22 @@ class Rules } //-------------------------------------------------------------------- + + /** + * Checks a URL to ensure it's formed correctly. + * + * @param string $str + * + * @return boolean + */ + public function validate_url(string $str = null): bool + { + if (empty($str)) { + return false; + } + + return filter_var($str, FILTER_VALIDATE_URL) !== false; + } + + //-------------------------------------------------------------------- } diff --git a/app/Views/_assets/icons/arrow-right.svg b/app/Views/_assets/icons/arrow-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..f46779f73370a634e78368fae4b3df422256e2da --- /dev/null +++ b/app/Views/_assets/icons/arrow-right.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M16.172 11l-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/check.svg b/app/Views/_assets/icons/check.svg new file mode 100644 index 0000000000000000000000000000000000000000..a28368fca94caa5cfc293a10255316a14cb05b2d --- /dev/null +++ b/app/Views/_assets/icons/check.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/> + </g> +</svg> diff --git a/app/Views/_assets/install.ts b/app/Views/_assets/install.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3bb9d53f46afa20fc4ca5e8153724c9aec8bc00 --- /dev/null +++ b/app/Views/_assets/install.ts @@ -0,0 +1,3 @@ +import Tooltip from "./modules/Tooltip"; + +Tooltip(); diff --git a/app/Views/install/_layout.php b/app/Views/install/_layout.php index 20bd2d2a4f94ac751b274b9cf9afba0747c024eb..6cd4cb1e5a918fe3c4deb7f2653c01235b3e2237 100644 --- a/app/Views/install/_layout.php +++ b/app/Views/install/_layout.php @@ -8,6 +8,7 @@ <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"/> + <script src="/assets/install.js" type="module" defer></script> </head> <body class="flex flex-col min-h-screen mx-auto"> @@ -16,7 +17,7 @@ Castopod installer </div> </header> - <main class="container flex-1 px-4 py-10 mx-auto"> + <main class="container flex flex-col items-center justify-center flex-1 px-4 py-10 mx-auto"> <?= view('_message_block') ?> <?= $this->renderSection('content') ?> </main> diff --git a/app/Views/install/cache_config.php b/app/Views/install/cache_config.php new file mode 100644 index 0000000000000000000000000000000000000000..ddd95d8e4b27962c62e05d35d31bcd5230f380d7 --- /dev/null +++ b/app/Views/install/cache_config.php @@ -0,0 +1,44 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<?= form_open(route_to('cache-config'), [ + 'class' => 'flex flex-col max-w-sm w-full', +]) ?> +<?= csrf_field() ?> + +<h1 class="mb-4 text-xl"><span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider text-green-700 border-4 border-green-500 rounded-full">3/4</span><?= lang( + 'Install.form.cache_config' +) ?></h1> + +<p class="mb-4 text-sm text-gray-600"><?= lang( + 'Install.form.cache_config_hint' +) ?></p> + +<?= form_label(lang('Install.form.cache_handler'), 'db_prefix') ?> +<?= form_dropdown( + 'cache_handler', + [ + 'file' => lang('Install.form.cacheHandlerOptions.file'), + 'redis' => lang('Install.form.cacheHandlerOptions.redis'), + 'memcached' => lang('Install.form.cacheHandlerOptions.memcached'), + ], + old('cache_handler', 'file'), + [ + 'id' => 'cache_handler', + 'name' => 'cache_handler', + 'class' => 'form-select mb-6', + 'value' => config('Database')->default['DBPrefix'], + ] +) ?> + +<?= button( + lang('Install.form.next') . icon('arrow-right', 'ml-2'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/app/Views/install/create_superadmin.php b/app/Views/install/create_superadmin.php new file mode 100644 index 0000000000000000000000000000000000000000..6a560773329604f2ec20b6611a6835f4d900235a --- /dev/null +++ b/app/Views/install/create_superadmin.php @@ -0,0 +1,52 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<?= form_open(route_to('create-superadmin'), [ + 'class' => 'flex flex-col max-w-sm w-full', +]) ?> +<?= csrf_field() ?> + +<h1 class="mb-4 text-xl"><span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider text-green-700 border-4 border-green-500 rounded-full">4/4</span><?= lang( + 'Install.form.create_superadmin' +) ?></h1> + +<?= form_label(lang('Install.form.email'), 'email') ?> +<?= form_input([ + 'id' => 'email', + 'name' => 'email', + 'class' => 'form-input mb-4', + 'type' => 'email', + 'required' => 'required', + 'value' => old('email'), +]) ?> + +<?= form_label(lang('Install.form.username'), 'username') ?> +<?= form_input([ + 'id' => 'username', + 'name' => 'username', + 'class' => 'form-input mb-4', + 'required' => 'required', + 'value' => old('username'), +]) ?> + +<?= form_label(lang('Install.form.password'), 'password') ?> +<?= form_input([ + 'id' => 'password', + 'name' => 'password', + 'class' => 'form-input mb-4', + 'type' => 'password', + 'required' => 'required', + 'autocomplete' => 'new-password', +]) ?> + +<?= button( + icon('check', 'mr-2') . lang('Install.form.submit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/app/Views/install/database_config.php b/app/Views/install/database_config.php new file mode 100644 index 0000000000000000000000000000000000000000..169419576d033373bef739ae9ae01f2ba064c460 --- /dev/null +++ b/app/Views/install/database_config.php @@ -0,0 +1,80 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<?= form_open(route_to('database-config'), [ + 'class' => 'flex flex-col max-w-sm w-full', + 'autocomplete' => 'off', +]) ?> +<?= csrf_field() ?> + +<h1 class="mb-2 text-xl"><span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider text-green-700 border-4 border-green-500 rounded-full">2/4</span><?= lang( + 'Install.form.database_config' +) ?></h1> + +<p class="mb-4 text-sm text-gray-600"><?= lang( + 'Install.form.database_config_hint' +) ?></p> + +<?= form_label(lang('Install.form.db_hostname'), 'db_hostname') ?> +<?= form_input([ + 'id' => 'db_hostname', + 'name' => 'db_hostname', + 'class' => 'form-input mb-4', + 'value' => old('db_hostname', config('Database')->default['hostname']), + 'required' => 'required', +]) ?> + +<?= form_label(lang('Install.form.db_name'), 'db_name') ?> +<?= form_input([ + 'id' => 'db_name', + 'name' => 'db_name', + 'class' => 'form-input mb-4', + 'value' => old('db_name', config('Database')->default['database']), + 'required' => 'required', +]) ?> + +<?= form_label(lang('Install.form.db_username'), 'db_username') ?> +<?= form_input([ + 'id' => 'db_username', + 'name' => 'db_username', + 'class' => 'form-input mb-4', + 'value' => old('db_username', config('Database')->default['username']), + 'required' => 'required', + 'autocomplete' => 'off', +]) ?> + +<?= form_label(lang('Install.form.db_password'), 'db_password') ?> +<?= form_input([ + 'id' => 'db_password', + 'name' => 'db_password', + 'class' => 'form-input mb-4', + 'value' => old('db_password', config('Database')->default['password']), + 'type' => 'password', + 'required' => 'required', + 'autocomplete' => 'off', +]) ?> + +<?= form_label( + lang('Install.form.db_prefix'), + 'db_prefix', + [], + lang('Install.form.db_prefix_hint') +) ?> +<?= form_input([ + 'id' => 'db_prefix', + 'name' => 'db_prefix', + 'class' => 'form-input mb-6', + 'value' => old('db_prefix', config('Database')->default['DBPrefix']), +]) ?> + +<?= button( + lang('Install.form.next') . icon('arrow-right', 'ml-2'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/app/Views/install/env.php b/app/Views/install/env.php deleted file mode 100644 index 25db9b5d7b33b621b15cc94659279567ec13e7da..0000000000000000000000000000000000000000 --- a/app/Views/install/env.php +++ /dev/null @@ -1,98 +0,0 @@ -<?= $this->extend('install/_layout') ?> - -<?= $this->section('content') ?> - -<?= form_open(route_to('generate-env'), [ - 'class' => 'flex flex-col max-w-sm mx-auto', -]) ?> -<?= csrf_field() ?> - -<?= 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, - 'required' => 'required', - ]) ?> - - <?= 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, - 'required' => 'required', - ]) ?> - - <?= form_label(lang('Install.form.auth_gateway'), 'auth_gateway') ?> - <?= form_input([ - 'id' => 'auth_gateway', - 'name' => 'auth_gateway', - 'class' => 'form-input', - 'value' => config('App')->authGateway, - 'required' => 'required', - ]) ?> -<?= 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'], - 'required' => 'required', - ]) ?> - - <?= 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'], - 'required' => 'required', - ]) ?> - - <?= 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'], - 'required' => 'required', - ]) ?> - - <?= 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'], - 'required' => 'required', - ]) ?> - - <?= 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() ?> - -<?= button( - lang('Install.form.submit_install'), - null, - ['variant' => 'primary'], - ['type' => 'submit', 'class' => 'self-end'] -) ?> - -<?= form_close() ?> - -<?= $this->endSection() ?> diff --git a/app/Views/install/error.php b/app/Views/install/error.php deleted file mode 100644 index b4f8bf0ac0e0dbe4e8f4427567ac8acf9094cf86..0000000000000000000000000000000000000000 --- a/app/Views/install/error.php +++ /dev/null @@ -1,9 +0,0 @@ -<?= $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/instance_config.php b/app/Views/install/instance_config.php new file mode 100644 index 0000000000000000000000000000000000000000..19416b9f89eb5b042d804ad83aad457750d8c042 --- /dev/null +++ b/app/Views/install/instance_config.php @@ -0,0 +1,59 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<form action="<?= '/' . + config('App')->installGateway . + '/instance-config' ?>" class="flex flex-col w-full max-w-sm" method="post" accept-charset="utf-8"> +<?= csrf_field() ?> + +<h1 class="mb-4 text-xl"><span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider text-green-700 border-4 border-green-500 rounded-full">1/4</span><?= lang( + 'Install.form.instance_config' +) ?></h1> +<?= form_label(lang('Install.form.hostname'), 'hostname') ?> +<?= form_input([ + 'id' => 'hostname', + 'name' => 'hostname', + 'class' => 'form-input mb-4', + 'value' => old('hostname', set_value(host_url(), config('App')->baseURL)), + 'required' => 'required', +]) ?> + +<?= form_label( + lang('Install.form.admin_gateway'), + 'admin_gateway', + [], + lang('Install.form.admin_gateway_hint') +) ?> +<?= form_input([ + 'id' => 'admin_gateway', + 'name' => 'admin_gateway', + 'class' => 'form-input mb-4', + 'value' => old('admin_gateway', config('App')->adminGateway), + 'required' => 'required', +]) ?> + +<?= form_label( + lang('Install.form.auth_gateway'), + 'auth_gateway', + [], + lang('Install.form.auth_gateway_hint') +) ?> +<?= form_input([ + 'id' => 'auth_gateway', + 'name' => 'auth_gateway', + 'class' => 'form-input mb-6', + 'value' => old('auth_gateway', config('App')->authGateway), + 'required' => 'required', +]) ?> + +<?= button( + lang('Install.form.next') . icon('arrow-right', 'ml-2'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + +<?= form_close() ?> + +<?= $this->endSection() ?> diff --git a/app/Views/install/manual_config.php b/app/Views/install/manual_config.php new file mode 100644 index 0000000000000000000000000000000000000000..446eee8631d4f9edb9d70c8f6f4a03ffa1bc4976 --- /dev/null +++ b/app/Views/install/manual_config.php @@ -0,0 +1,14 @@ +<?= $this->extend('install/_layout') ?> + +<?= $this->section('content') ?> + +<h1 class="mb-2 text-xl font-bold"><?= lang('Install.manual_config') ?></h1> +<div class="inline-flex items-baseline max-w-2xl px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700"> +<?= icon('alert', 'mr-2 flex-shrink-0') . lang('Install.messages.writeError') ?> +</div> +<p class="mb-4 font-semibold text-gray-600"><?= lang( + 'Install.manual_config_subtitle' +) ?></p> + +<?= $this->endSection() +?> diff --git a/app/Views/install/superadmin.php b/app/Views/install/superadmin.php deleted file mode 100644 index 6b32c32cc355023337f2fe05b58ffd81bce4eb8b..0000000000000000000000000000000000000000 --- a/app/Views/install/superadmin.php +++ /dev/null @@ -1,53 +0,0 @@ -<?= $this->extend('install/_layout') ?> - -<?= $this->section('content') ?> - -<?= form_open(route_to('create-superadmin'), [ - 'class' => 'flex flex-col max-w-sm mx-auto', -]) ?> -<?= csrf_field() ?> - -<?= 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', - 'required' => 'required', - 'value' => old('email'), - ]) ?> - - <?= form_label(lang('Install.form.username'), 'username') ?> - <?= form_input([ - 'id' => 'username', - 'name' => 'username', - 'class' => 'form-input mb-4', - 'required' => 'required', - 'value' => old('username'), - ]) ?> - - <?= form_label(lang('Install.form.password'), 'password') ?> - <?= form_input([ - 'id' => 'password', - 'name' => 'password', - 'class' => 'form-input mb-4', - 'type' => 'password', - 'required' => 'required', - 'autocomplete' => 'new-password', - ]) ?> -<?= form_fieldset_close() ?> - -<?= button( - lang('Install.form.submit_create_superadmin'), - null, - ['variant' => 'primary'], - ['type' => 'submit', 'class' => 'self-end'] -) ?> - -<?= form_close() ?> - -<?= $this->endSection() ?>