diff --git a/.gitignore b/.gitignore index 47f4c373..4ed935bc 100644 --- a/.gitignore +++ b/.gitignore @@ -137,9 +137,11 @@ node_modules # public folder public/* +public/media/site !public/media !public/.htaccess !public/favicon.ico +!public/icon* !public/index.php !public/robots.txt diff --git a/app/Config/App.php b/app/Config/App.php index df445db0..4d2227c0 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -427,4 +427,24 @@ class App extends BaseConfig * Defines the root folder for media files storage */ public string $mediaRoot = 'media'; + + /** + * -------------------------------------------------------------------------- + * Instance / Site Config + * -------------------------------------------------------------------------- + */ + public string $siteName = 'Castopod'; + + public string $siteDescription = 'Castopod Host is an open-source hosting platform made for podcasters who want engage and interact with their audience.'; + + /** + * @var array + */ + public array $siteIcon = [ + 'ico' => '/favicon.ico', + '64' => '/icon-64.png', + '180' => '/icon-180.png', + '192' => '/icon-192.png', + '512' => '/icon-512.png', + ]; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 45eff6b9..7d2ca19f 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -48,9 +48,13 @@ $routes->addPlaceholder( * -------------------------------------------------------------------- */ +$routes->get('manifest.webmanifest', 'WebmanifestController', [ + 'as' => 'webmanifest', +]); + // We get a performance increase by specifying the default // route since we don't have to scan directories. -$routes->get('/', 'HomeController::index', [ +$routes->get('/', 'HomeController', [ 'as' => 'home', ]); diff --git a/app/Controllers/WebmanifestController.php b/app/Controllers/WebmanifestController.php new file mode 100644 index 00000000..dd320e44 --- /dev/null +++ b/app/Controllers/WebmanifestController.php @@ -0,0 +1,45 @@ + service('settings') + ->get('App.siteName'), + 'description' => service('settings') + ->get('App.siteDescription'), + 'display' => 'minimal-ui', + 'theme_color' => '#009486', + 'icons' => [ + [ + 'src' => service('settings') + ->get('App.siteIcon')['192'], + 'type' => 'image/png', + 'sizes' => '192x192', + ], + [ + 'src' => service('settings') + ->get('App.siteIcon')['512'], + 'type' => 'image/png', + 'sizes' => '512x512', + ], + ], + ]; + + return $this->response->setJSON($webmanifest); + } +} diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index 2fb6fb6a..eb9af6a7 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -40,6 +40,18 @@ class AuthSeeder extends Seeder * @var array[]> */ protected array $permissions = [ + 'settings' => [ + [ + 'name' => 'view', + 'description' => 'View settings options', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage', + 'description' => 'Update general settings', + 'has_permission' => ['superadmin'], + ], + ], 'users' => [ [ 'name' => 'create', diff --git a/app/Helpers/media_helper.php b/app/Helpers/media_helper.php index 312af42a..b9a1c65b 100644 --- a/app/Helpers/media_helper.php +++ b/app/Helpers/media_helper.php @@ -29,6 +29,9 @@ if (! function_exists('save_media')) { if (! file_exists($mediaRoot)) { mkdir($mediaRoot, 0777, true); + } + + if (! file_exists($mediaRoot . '/index.html')) { touch($mediaRoot . '/index.html'); } diff --git a/composer.json b/composer.json index 3ca24a4f..b7dffcaf 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,17 @@ "geoip2/geoip2": "^v2.11.0", "myth/auth": "dev-develop", "codeigniter4/codeigniter4": "dev-develop", - "league/commonmark": "^1.6.6", + "league/commonmark": "^v1.6.6", "vlucas/phpdotenv": "^v5.3.0", - "league/html-to-markdown": "^5.0.0", + "league/html-to-markdown": "^v5.0.1", "opawg/user-agents-php": "^v1.0", "podlibre/ipcat": "^v1.0", "podlibre/podcast-namespace": "^v1.0.6", "phpseclib/phpseclib": "~2.0.30", "michalsn/codeigniter4-uuid": "dev-develop", - "essence/essence": "^3.5.4" + "essence/essence": "^3.5.4", + "codeigniter4/settings": "dev-develop", + "chrisjean/php-ico": "^1.0" }, "require-dev": { "mikey179/vfsstream": "^v1.6.8", diff --git a/composer.lock b/composer.lock index 03baaec3..82b1287a 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": "af4a438816f7adbbd6950d93ed333e9f", + "content-hash": "f35a050323bdc632cd550f9d13f0679c", "packages": [ { "name": "brick/math", @@ -60,6 +60,46 @@ ], "time": "2021-01-20T22:51:39+00:00" }, + { + "name": "chrisjean/php-ico", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/chrisbliss18/php-ico.git", + "reference": "ccd5c0d56554f3ddcd7a823e695be83e0d1e43b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chrisbliss18/php-ico/zipball/ccd5c0d56554f3ddcd7a823e695be83e0d1e43b6", + "reference": "ccd5c0d56554f3ddcd7a823e695be83e0d1e43b6", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "php": ">=5.2.4" + }, + "type": "library", + "autoload": { + "classmap": ["class-php-ico.php"] + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["GPL-2.0+"], + "authors": [ + { + "name": "Chris Jean", + "homepage": "https://chrisjean.com", + "role": "Developer" + } + ], + "description": "An easy-to-use library to generate valid ICO files.", + "homepage": "https://github.com/chrisbliss18/php-ico", + "keywords": ["favicon", "ico"], + "support": { + "issues": "https://github.com/chrisbliss18/php-ico/issues", + "source": "https://github.com/chrisbliss18/php-ico" + }, + "time": "2016-09-27T22:00:56+00:00" + }, { "name": "codeigniter4/codeigniter4", "version": "dev-develop", @@ -137,6 +177,59 @@ }, "time": "2021-06-10T06:40:05+00:00" }, + { + "name": "codeigniter4/settings", + "version": "dev-develop", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/settings.git", + "reference": "5d758e5e0a3f9dda9f66303d82ccfbb82e979577" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/settings/zipball/5d758e5e0a3f9dda9f66303d82ccfbb82e979577", + "reference": "5d758e5e0a3f9dda9f66303d82ccfbb82e979577", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "codeigniter4/codeigniter4": "dev-develop", + "fakerphp/faker": "^1.9", + "mockery/mockery": "^1.0", + "nexusphp/tachycardia": "^1.0", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Sparks\\Settings\\": "src" + }, + "exclude-from-classmap": ["**/Database/Migrations/**"] + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Lonnie Ezell", + "email": "lonnieje@gmail.com", + "role": "Developer" + } + ], + "description": "Settings library for CodeIgniter 4", + "homepage": "https://github.com/codeigniter4/settings", + "keywords": ["Settings", "codeigniter", "codeigniter4"], + "support": { + "issues": "https://github.com/codeigniter4/settings/issues", + "source": "https://github.com/codeigniter4/settings/tree/develop" + }, + "time": "2021-08-16T05:07:59+00:00" + }, { "name": "composer/ca-bundle", "version": "1.2.10", @@ -494,16 +587,16 @@ }, { "name": "james-heinrich/getid3", - "version": "2.0.x-dev", + "version": "v2.0.0-beta4", "source": { "type": "git", "url": "https://github.com/JamesHeinrich/getID3.git", - "reference": "ee238d552571c6029898b087d5fc95df826418d6" + "reference": "5ad79104e937e7d9c8a9141a97e1f063dd1123f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/ee238d552571c6029898b087d5fc95df826418d6", - "reference": "ee238d552571c6029898b087d5fc95df826418d6", + "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/5ad79104e937e7d9c8a9141a97e1f063dd1123f8", + "reference": "5ad79104e937e7d9c8a9141a97e1f063dd1123f8", "shasum": "" }, "require": { @@ -562,9 +655,9 @@ "keywords": ["audio", "codecs", "id3", "metadata", "tags", "video"], "support": { "issues": "https://github.com/JamesHeinrich/getID3/issues", - "source": "https://github.com/JamesHeinrich/getID3/tree/2.0" + "source": "https://github.com/JamesHeinrich/getID3/tree/v2.0.0-beta4" }, - "time": "2021-12-15T17:29:14+00:00" + "time": "2021-10-06T16:23:45+00:00" }, { "name": "kint-php/kint", @@ -745,16 +838,16 @@ }, { "name": "league/commonmark", - "version": "1.6.2", + "version": "1.6.6", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb" + "reference": "c4228d11e30d7493c6836d20872f9582d8ba6dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/7d70d2f19c84bcc16275ea47edabee24747352eb", - "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4228d11e30d7493c6836d20872f9582d8ba6dcf", + "reference": "c4228d11e30d7493c6836d20872f9582d8ba6dcf", "shasum": "" }, "require": { @@ -772,7 +865,7 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "~1.4", "mikehaertl/php-shellcommand": "^1.4", - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^0.12.90", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", "scrutinizer/ocular": "^1.5", "symfony/finder": "^4.2" @@ -838,37 +931,40 @@ "type": "tidelift" } ], - "time": "2021-05-12T11:39:41+00:00" + "time": "2021-07-17T17:13:23+00:00" }, { "name": "league/html-to-markdown", - "version": "4.10.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "0868ae7a552e809e5cd8f93ba022071640408e88" + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0868ae7a552e809e5cd8f93ba022071640408e88", - "reference": "0868ae7a552e809e5cd8f93ba022071640408e88", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e5600a2c5ce7b7571b16732c7086940f56f7abec", + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec", "shasum": "" }, "require": { "ext-dom": "*", "ext-xml": "*", - "php": ">=5.3.3" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "mikehaertl/php-shellcommand": "~1.1.0", - "phpunit/phpunit": "^4.8|^5.7", - "scrutinizer/ocular": "~1.1" + "mikehaertl/php-shellcommand": "^1.1.0", + "phpstan/phpstan": "^0.12.82", + "phpunit/phpunit": "^8.5 || ^9.2", + "scrutinizer/ocular": "^1.6", + "unleashedtech/php-coding-standard": "^2.7", + "vimeo/psalm": "^4.6" }, "bin": ["bin/html-to-markdown"], "type": "library", "extra": { "branch-alias": { - "dev-master": "4.10-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -897,7 +993,7 @@ "keywords": ["html", "markdown"], "support": { "issues": "https://github.com/thephpleague/html-to-markdown/issues", - "source": "https://github.com/thephpleague/html-to-markdown/tree/4.10.0" + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.1" }, "funding": [ { @@ -913,11 +1009,11 @@ "type": "github" }, { - "url": "https://www.patreon.com/colinodell", - "type": "patreon" + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" } ], - "time": "2020-07-01T00:34:03+00:00" + "time": "2021-09-17T20:00:27+00:00" }, { "name": "maxmind-db/reader", @@ -7457,7 +7553,8 @@ "james-heinrich/getid3": 20, "myth/auth": 20, "codeigniter4/codeigniter4": 20, - "michalsn/codeigniter4-uuid": 20 + "michalsn/codeigniter4-uuid": 20, + "codeigniter4/settings": 20 }, "prefer-stable": true, "prefer-lowest": false, @@ -7465,5 +7562,5 @@ "php": "^8.0" }, "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/ecs.php b/ecs.php index 939cae29..a7d40715 100644 --- a/ecs.php +++ b/ecs.php @@ -17,7 +17,6 @@ return static function (ContainerConfigurator $containerConfigurator): void { __DIR__ . '/themes', __DIR__ . '/tests', __DIR__ . '/public', - __DIR__ . '/public', ]); $parameters->set(Option::SKIP, [ diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php index d259ce90..edda5e81 100644 --- a/modules/Admin/Config/Routes.php +++ b/modules/Admin/Config/Routes.php @@ -18,6 +18,21 @@ $routes->group( 'as' => 'admin', ]); + $routes->group('settings', function ($routes): void { + $routes->get('/', 'SettingsController', [ + 'as' => 'settings-general', + 'filter' => 'permission:settings-manage', + ]); + $routes->post('instance', 'SettingsController::attemptInstanceEdit', [ + 'as' => 'settings-instance', + 'filter' => 'permission:settings-manage', + ]); + $routes->get('instance-delete-icon', 'SettingsController::deleteIcon', [ + 'as' => 'settings-instance-delete-icon', + 'filter' => 'permission:settings-manage', + ]); + }); + $routes->group('persons', function ($routes): void { $routes->get('/', 'PersonController', [ 'as' => 'person-list', diff --git a/modules/Admin/Controllers/SettingsController.php b/modules/Admin/Controllers/SettingsController.php new file mode 100644 index 00000000..1308df71 --- /dev/null +++ b/modules/Admin/Controllers/SettingsController.php @@ -0,0 +1,105 @@ + + 'is_image[site_icon]|ext_in[site_icon,png,jpeg]|is_image_squared[site_icon]|min_dims[image,512,512]|permit_empty', + ]; + + if (! $this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + $siteName = $this->request->getPost('site_name'); + if ($siteName !== service('settings')->get('App.siteName')) { + service('settings')->set('App.siteName', $siteName); + } + + $siteDescription = $this->request->getPost('site_description'); + if ($siteDescription !== service('settings')->get('App.siteDescription')) { + service('settings')->set('App.siteDescription', $siteDescription); + } + + $siteIconFile = $this->request->getFile('site_icon'); + if ($siteIconFile !== null && $siteIconFile->isValid()) { + helper(['filesystem', 'media']); + + // delete site folder in media before repopulating it + delete_files(ROOTPATH . 'public/media/site/'); + + // save original in disk + $originalFilename = save_media($siteIconFile, 'site', 'icon'); + + // convert jpeg image to png if not + if ($siteIconFile->getClientMimeType() !== 'image/png') { + service('image')->withFile(ROOTPATH . 'public/media/' . $originalFilename) + ->convert(IMAGETYPE_JPEG) + ->save(ROOTPATH . 'public/media/site/icon.png'); + } + + // generate random hash to use as a suffix to renew browser cache + $randomHash = substr(bin2hex(random_bytes(18)), 0, 8); + + // generate ico + $ico_lib = new PHP_ICO(); + $ico_lib->add_image(ROOTPATH . 'public/media/site/icon.png', [[32, 32], [64, 64]]); + $ico_lib->save_ico(ROOTPATH . "public/media/site/favicon.{$randomHash}.ico"); + + // resize original to needed sizes + foreach ([64, 180, 192, 512] as $size) { + service('image') + ->withFile(ROOTPATH . 'public/media/site/icon.png') + ->resize($size, $size) + ->save(ROOTPATH . "public/media/site/icon-{$size}.{$randomHash}.png"); + } + + service('settings') + ->set('App.siteIcon', [ + 'ico' => "/media/site/favicon.{$randomHash}.ico", + '64' => "/media/site/icon-64.{$randomHash}.png", + '180' => "/media/site/icon-180.{$randomHash}.png", + '192' => "/media/site/icon-192.{$randomHash}.png", + '512' => "/media/site/icon-512.{$randomHash}.png", + ]); + } + + return redirect()->back(); + } + + public function deleteIcon(): RedirectResponse + { + helper('filesystem'); + // delete site folder in media + delete_files(ROOTPATH . 'public/media/site/'); + + service('settings') + ->forget('App.siteIcon'); + + return redirect()->back(); + } +} diff --git a/modules/Admin/Language/en/AdminNavigation.php b/modules/Admin/Language/en/AdminNavigation.php index 68cbefb3..c2e8e4b6 100644 --- a/modules/Admin/Language/en/AdminNavigation.php +++ b/modules/Admin/Language/en/AdminNavigation.php @@ -29,6 +29,8 @@ return [ 'pages' => 'Pages', 'page-list' => 'All pages', 'page-create' => 'New Page', + 'settings' => 'Settings', + 'settings-general' => 'General', 'account' => [ 'my-account' => 'My account', 'change-password' => 'Change password', diff --git a/modules/Admin/Language/en/Breadcrumb.php b/modules/Admin/Language/en/Breadcrumb.php index 9a1ef1c2..63564f79 100644 --- a/modules/Admin/Language/en/Breadcrumb.php +++ b/modules/Admin/Language/en/Breadcrumb.php @@ -16,6 +16,7 @@ return [ 'episodes' => 'episodes', 'contributors' => 'contributors', 'pages' => 'pages', + 'settings' => 'settings', 'add' => 'add', 'new' => 'new', 'edit' => 'edit', diff --git a/modules/Admin/Language/en/Settings.php b/modules/Admin/Language/en/Settings.php new file mode 100644 index 00000000..8864c87f --- /dev/null +++ b/modules/Admin/Language/en/Settings.php @@ -0,0 +1,23 @@ + 'General settings', + 'form' => [ + 'site_section_title' => 'Instance', + 'site_icon' => 'Site icon', + 'site_icon_delete' => 'Delete site icon', + 'site_icon_hint' => 'Site icons are what you see on your browser tabs, bookmarks bar, and when you add a website as a shortcut on mobile devices.', + 'site_icon_helper' => 'Icon must be squared with at least 512px wide and tall.', + 'site_name' => 'Site name', + 'site_description' => 'Site description', + 'submit' => 'Save', + ], +]; diff --git a/modules/Admin/Language/fr/AdminNavigation.php b/modules/Admin/Language/fr/AdminNavigation.php index 357b64ff..b85b1a30 100644 --- a/modules/Admin/Language/fr/AdminNavigation.php +++ b/modules/Admin/Language/fr/AdminNavigation.php @@ -28,6 +28,8 @@ return [ 'pages' => 'Pages', 'page-list' => 'Toutes les pages', 'page-create' => 'Créer une page', + 'settings' => 'Paramètres', + 'settings-general' => 'Général', 'account' => [ 'my-account' => 'Mon compte', 'change-password' => 'Modifier le mot de passe', diff --git a/modules/Admin/Language/fr/Settings.php b/modules/Admin/Language/fr/Settings.php new file mode 100644 index 00000000..e68c87c7 --- /dev/null +++ b/modules/Admin/Language/fr/Settings.php @@ -0,0 +1,24 @@ + 'Paramètres généraux', + 'form' => [ + 'site_section_title' => 'Instance', + 'site_section_subtitle' => 'configuration de l’instance', + 'site_icon' => 'Favicon du site', + 'site_icon_delete' => 'Supprimer la favicon du site', + 'site_icon_hint' => 'Les favicons sont ce que vous voyez sur les onglets de votre navigateur, dans votre barre de favoris, et lorsque vous ajoutez un site web en raccourci sur des appareils mobiles.', + 'site_icon_helper' => 'La favicon doit être carrée et avoir au moins 512px de largeur et de hauteur.', + 'site_name' => 'Titre du site', + 'site_description' => 'Description du site', + 'submit' => 'Save', + ], +]; diff --git a/modules/Install/Controllers/InstallController.php b/modules/Install/Controllers/InstallController.php index 59bad8c9..5a65ce44 100644 --- a/modules/Install/Controllers/InstallController.php +++ b/modules/Install/Controllers/InstallController.php @@ -243,6 +243,8 @@ class InstallController extends Controller { $migrations = Services::migrations(); + $migrations->setNamespace('Sparks\Settings') + ->latest(); $migrations->setNamespace('Myth\Auth') ->latest(); $migrations->setNamespace('Modules\Fediverse') diff --git a/public/favicon.ico b/public/favicon.ico index b844d1d0..9bf9b97a 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icon-180.png b/public/icon-180.png new file mode 100644 index 00000000..937c4c6c Binary files /dev/null and b/public/icon-180.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 00000000..7864f7e9 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 00000000..9998e36a Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/icon-64.png b/public/icon-64.png new file mode 100644 index 00000000..cf8d2cf1 Binary files /dev/null and b/public/icon-64.png differ diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 00000000..3c182ad4 Binary files /dev/null and b/public/icon.png differ diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index 3b673615..17274d51 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -7,7 +7,10 @@ <?= $this->renderSection('title') ?> | Castopod Admin - + + + asset('styles/index.css', 'css') ?> diff --git a/themes/cp_admin/_sidebar.php b/themes/cp_admin/_sidebar.php index 0a1a7f41..dd8cb12a 100644 --- a/themes/cp_admin/_sidebar.php +++ b/themes/cp_admin/_sidebar.php @@ -22,6 +22,10 @@ $navigation = [ 'items' => ['page-list', 'page-create'], ], + 'settings' => [ + 'icon' => 'settings', + 'items' => ['settings-general'], + ], ]; ?>