From a1a28de702c8488a4f92ec05e42e3cdead0d1edd Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Fri, 14 Aug 2020 18:27:57 +0000 Subject: [PATCH] refactor: rewrite form pages using form helper - add installGateway to app config - update route names and groups - remove `author_name` and `author_email` from `episodes` table - remove `author_name` and `author_email` from `podcasts` table - remove `owner_id` + add `created_by` and `updated_by` fields in `podcasts` and `episodes` tables - remove unnecessary comments in database fields - remove confirm password inputs from auth forms for better ux - rename `pub_date` field to `published_at` and add publication time field in episode form closes #14, #28 --- app/Config/App.php | 8 + app/Config/Routes.php | 399 +++++++++--------- app/Controllers/Admin/Contributor.php | 44 +- app/Controllers/Admin/Episode.php | 37 +- app/Controllers/Admin/Myaccount.php | 10 +- app/Controllers/Admin/Podcast.php | 82 +++- app/Controllers/Admin/User.php | 27 +- app/Controllers/Auth.php | 167 ++++++++ .../2020-05-30-101000_add_languages.php | 1 - .../2020-05-30-101500_add_podcasts.php | 60 +-- .../2020-06-05-170000_add_episodes.php | 57 +-- .../2020-06-05-190000_add_platforms.php | 13 - .../2020-06-08-160000_add_platform_links.php | 5 - ...0000_add_analytics_episodes_by_country.php | 6 - ...10000_add_analytics_episodes_by_player.php | 6 - ...0000_add_analytics_podcasts_by_country.php | 4 - ...10000_add_analytics_podcasts_by_player.php | 5 - ...10000_add_analytics_unknown_useragents.php | 3 - ...10000_add_analytics_website_by_browser.php | 5 - ...10000_add_analytics_website_by_country.php | 4 - ...10000_add_analytics_website_by_referer.php | 4 - app/Entities/Episode.php | 37 +- app/Entities/Podcast.php | 52 +-- app/Helpers/id3_helper.php | 4 +- app/Helpers/rss_helper.php | 21 +- app/Language/en/AdminNavigation.php | 10 +- app/Language/en/Contributor.php | 3 + app/Language/en/Episode.php | 14 +- app/Language/en/MyAccount.php | 2 + app/Language/en/Podcast.php | 5 +- app/Language/en/User.php | 6 +- app/Models/EpisodeModel.php | 15 +- app/Models/PodcastModel.php | 15 +- app/Views/admin/_header.php | 8 +- app/Views/admin/_partials/_episode-card.php | 8 +- app/Views/admin/_partials/_podcast-card.php | 6 +- app/Views/admin/_sidenav.php | 6 +- app/Views/admin/contributor/add.php | 59 ++- app/Views/admin/contributor/edit.php | 41 +- app/Views/admin/contributor/list.php | 10 +- app/Views/admin/episode/create.php | 221 ++++++---- app/Views/admin/episode/edit.php | 223 ++++++---- app/Views/admin/episode/list.php | 2 +- app/Views/admin/episode/view.php | 4 +- .../admin/my_account/change_password.php | 45 +- app/Views/admin/podcast/create.php | 287 +++++++------ app/Views/admin/podcast/edit.php | 276 +++++++----- app/Views/admin/podcast/list.php | 2 +- app/Views/admin/podcast/view.php | 8 +- app/Views/admin/user/create.php | 53 ++- app/Views/admin/user/edit.php | 36 +- app/Views/admin/user/list.php | 8 +- app/Views/auth/_layout.php | 11 +- app/Views/auth/forgot.php | 30 +- app/Views/auth/login.php | 39 +- app/Views/auth/register.php | 64 +-- app/Views/auth/reset.php | 55 ++- app/Views/install/env.php | 10 +- app/Views/install/superadmin.php | 11 +- 59 files changed, 1538 insertions(+), 1116 deletions(-) create mode 100644 app/Controllers/Auth.php diff --git a/app/Config/App.php b/app/Config/App.php index f05de3d8..5dd608b3 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -290,4 +290,12 @@ class App extends BaseConfig | Defines a base route for all authentication related pages */ public $authGateway = 'cp-auth'; + + /* + |-------------------------------------------------------------------------- + | Install gateway + |-------------------------------------------------------------------------- + | Defines a base route for instance installation + */ + public $installGateway = 'cp-install'; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index dc8d3ce9..43df8b3b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -43,24 +43,23 @@ $routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}'); $routes->get('/', 'Home::index', ['as' => 'home']); // Install Wizard route -$routes->group('cp-install', function ($routes) { +$routes->group(config('App')->installGateway, function ($routes) { $routes->get('/', 'Install', ['as' => 'install']); $routes->post('generate-env', 'Install::attemptCreateEnv', [ - 'as' => 'install_generate_env', + 'as' => 'generate-env', ]); $routes->post('create-superadmin', 'Install::attemptCreateSuperAdmin', [ - 'as' => 'install_create_superadmin', + 'as' => 'create-superadmin', ]); }); // Public routes $routes->group('@(:podcastName)', function ($routes) { $routes->get('/', 'Podcast/$1', ['as' => 'podcast']); - - $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); - $routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [ + $routes->get('(:episodeSlug)', 'Episode/$1/$2', [ 'as' => 'episode', ]); + $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); }); // Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3) @@ -82,228 +81,218 @@ $routes->group( ]); $routes->get('my-podcasts', 'Podcast::myPodcasts', [ - 'as' => 'my_podcasts', - ]); - $routes->get('podcasts', 'Podcast::list', [ - 'as' => 'podcast_list', - ]); - $routes->get('podcasts/new', 'Podcast::create', [ - 'as' => 'podcast_create', - 'filter' => 'permission:podcasts-create', - ]); - $routes->post('podcasts/new', 'Podcast::attemptCreate', [ - 'filter' => 'permission:podcasts-create', + 'as' => 'my-podcasts', ]); - // Use ids in admin area to help permission and group lookups - $routes->group('podcasts/(:num)', function ($routes) { - $routes->get('/', 'Podcast::view/$1', [ - 'as' => 'podcast_view', - 'filter' => 'permission:podcasts-view,podcast-view', + // Podcasts + $routes->group('podcasts', function ($routes) { + $routes->get('/', 'Podcast::list', [ + 'as' => 'podcast-list', ]); - $routes->get('edit', 'Podcast::edit/$1', [ - 'as' => 'podcast_edit', - 'filter' => 'permission:podcasts-edit,podcast-edit', + $routes->get('new', 'Podcast::create', [ + 'as' => 'podcast-create', + 'filter' => 'permission:podcasts-create', ]); - $routes->post('edit', 'Podcast::attemptEdit/$1', [ - 'filter' => 'permission:podcasts-edit,podcast-edit', - ]); - $routes->add('delete', 'Podcast::delete/$1', [ - 'as' => 'podcast_delete', - 'filter' => 'permission:podcasts-edit,podcast-delete', + $routes->post('new', 'Podcast::attemptCreate', [ + 'filter' => 'permission:podcasts-create', ]); - // Podcast episodes - $routes->get('episodes', 'Episode::list/$1', [ - 'as' => 'episode_list', - 'filter' => 'permission:podcasts-view,podcast-view', - ]); - $routes->get('episodes/new', 'Episode::create/$1', [ - 'as' => 'episode_create', - 'filter' => - 'permission:episodes-create,podcast_episodes-create', - ]); - $routes->post('episodes/new', 'Episode::attemptCreate/$1', [ - 'filter' => - 'permission:episodes-create,podcast_episodes-create', - ]); + // Podcast + // Use ids in admin area to help permission and group lookups + $routes->group('(:num)', function ($routes) { + $routes->get('/', 'Podcast::view/$1', [ + 'as' => 'podcast-view', + 'filter' => 'permission:podcasts-view,podcast-view', + ]); + $routes->get('edit', 'Podcast::edit/$1', [ + 'as' => 'podcast-edit', + 'filter' => 'permission:podcasts-edit,podcast-edit', + ]); + $routes->post('edit', 'Podcast::attemptEdit/$1', [ + 'filter' => 'permission:podcasts-edit,podcast-edit', + ]); + $routes->add('delete', 'Podcast::delete/$1', [ + 'as' => 'podcast-delete', + 'filter' => 'permission:podcasts-edit,podcast-delete', + ]); - $routes->get('episodes/(:num)', 'Episode::view/$1/$2', [ - 'as' => 'episode_view', - 'filter' => 'permission:episodes-view,podcast_episodes-view', - ]); - $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [ - 'as' => 'episode_edit', - 'filter' => 'permission:episodes-edit,podcast_episodes-edit', - ]); - $routes->post( - 'episodes/(:num)/edit', - 'Episode::attemptEdit/$1/$2', - [ - 'filter' => - 'permission:episodes-edit,podcast_episodes-edit', - ] - ); - $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [ - 'as' => 'episode_delete', - 'filter' => - 'permission:episodes-delete,podcast_episodes-delete', - ]); + // Podcast episodes + $routes->group('episodes', function ($routes) { + $routes->get('/', 'Episode::list/$1', [ + 'as' => 'episode-list', + 'filter' => 'permission:podcasts-view,podcast-view', + ]); + $routes->get('new', 'Episode::create/$1', [ + 'as' => 'episode-create', + 'filter' => + 'permission:episodes-create,podcast_episodes-create', + ]); + $routes->post('new', 'Episode::attemptCreate/$1', [ + 'filter' => + 'permission:episodes-create,podcast_episodes-create', + ]); - // Podcast contributors - $routes->get('contributors', 'Contributor::list/$1', [ - 'as' => 'contributor_list', - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ]); - $routes->get('contributors/add', 'Contributor::add/$1', [ - 'as' => 'contributor_add', - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ]); - $routes->post('contributors/add', 'Contributor::attemptAdd/$1', [ - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ]); - $routes->get('contributors/(:num)', 'Contributor::view/$1/$2', [ - 'as' => 'contributor_view', - ]); - $routes->get( - 'contributors/(:num)/edit', - 'Contributor::edit/$1/$2', - [ - 'as' => 'contributor_edit', - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ] - ); - $routes->post( - 'contributors/(:num)/edit', - 'Contributor::attemptEdit/$1/$2', - [ - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ] - ); - $routes->add( - 'contributors/(:num)/remove', - 'Contributor::remove/$1/$2', - [ - 'as' => 'contributor_remove', - 'filter' => - 'permission:podcasts-manage_contributors,podcast-manage_contributors', - ] - ); + // Episode + $routes->group('(:num)', function ($routes) { + $routes->get('/', 'Episode::view/$1/$2', [ + 'as' => 'episode-view', + 'filter' => + 'permission:episodes-view,podcast_episodes-view', + ]); + $routes->get('edit', 'Episode::edit/$1/$2', [ + 'as' => 'episode-edit', + 'filter' => + 'permission:episodes-edit,podcast_episodes-edit', + ]); + $routes->post('edit', 'Episode::attemptEdit/$1/$2', [ + 'filter' => + 'permission:episodes-edit,podcast_episodes-edit', + ]); + $routes->add('delete', 'Episode::delete/$1/$2', [ + 'as' => 'episode-delete', + 'filter' => + 'permission:episodes-delete,podcast_episodes-delete', + ]); + }); + }); + + // Podcast contributors + $routes->group('contributors', function ($routes) { + $routes->get('/', 'Contributor::list/$1', [ + 'as' => 'contributor-list', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + $routes->get('add', 'Contributor::add/$1', [ + 'as' => 'contributor-add', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + $routes->post('add', 'Contributor::attemptAdd/$1', [ + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + + // Contributor + $routes->group('(:num)', function ($routes) { + $routes->get('/', 'Contributor::view/$1/$2', [ + 'as' => 'contributor-view', + ]); + $routes->get('edit', 'Contributor::edit/$1/$2', [ + 'as' => 'contributor-edit', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + $routes->post( + 'edit', + 'Contributor::attemptEdit/$1/$2', + [ + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ] + ); + $routes->add('remove', 'Contributor::remove/$1/$2', [ + 'as' => 'contributor-remove', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + }); + }); + }); }); // Users - $routes->get('users', 'User::list', [ - 'as' => 'user_list', - 'filter' => 'permission:users-list', - ]); - $routes->get('users/new', 'User::create', [ - 'as' => 'user_create', - 'filter' => 'permission:users-create', - ]); - $routes->get('users/(:num)', 'User::view/$1', [ - 'as' => 'user_view', - 'filter' => 'permission:users-view', - ]); - $routes->post('users/new', 'User::attemptCreate', [ - 'filter' => 'permission:users-create', - ]); - $routes->get('users/(:num)/edit', 'User::edit/$1', [ - 'as' => 'user_edit', - 'filter' => 'permission:users-manage_authorizations', - ]); - $routes->post('users/(:num)/edit', 'User::attemptEdit/$1', [ - 'filter' => 'permission:users-manage_authorizations', - ]); + $routes->group('users', function ($routes) { + $routes->get('/', 'User::list', [ + 'as' => 'user-list', + 'filter' => 'permission:users-list', + ]); + $routes->get('new', 'User::create', [ + 'as' => 'user-create', + 'filter' => 'permission:users-create', + ]); + $routes->post('new', 'User::attemptCreate', [ + 'filter' => 'permission:users-create', + ]); - $routes->add('users/(:num)/ban', 'User::ban/$1', [ - 'as' => 'user_ban', - 'filter' => 'permission:users-manage_bans', - ]); - $routes->add('users/(:num)/unban', 'User::unBan/$1', [ - 'as' => 'user_unban', - 'filter' => 'permission:users-manage_bans', - ]); - $routes->add( - 'users/(:num)/force-pass-reset', - 'User::forcePassReset/$1', - [ - 'as' => 'user_force_pass_reset', - 'filter' => 'permission:users-force_pass_reset', - ] - ); - - $routes->add('users/(:num)/delete', 'User::delete/$1', [ - 'as' => 'user_delete', - 'filter' => 'permission:users-delete', - ]); + // User + $routes->group('(:num)', function ($routes) { + $routes->get('/', 'User::view/$1', [ + 'as' => 'user-view', + 'filter' => 'permission:users-view', + ]); + $routes->get('edit', 'User::edit/$1', [ + 'as' => 'user-edit', + 'filter' => 'permission:users-manage_authorizations', + ]); + $routes->post('edit', 'User::attemptEdit/$1', [ + 'filter' => 'permission:users-manage_authorizations', + ]); + $routes->add('ban', 'User::ban/$1', [ + 'as' => 'user-ban', + 'filter' => 'permission:users-manage_bans', + ]); + $routes->add('unban', 'User::unBan/$1', [ + 'as' => 'user-unban', + 'filter' => 'permission:users-manage_bans', + ]); + $routes->add('force-pass-reset', 'User::forcePassReset/$1', [ + 'as' => 'user-force_pass_reset', + 'filter' => 'permission:users-force_pass_reset', + ]); + $routes->add('delete', 'User::delete/$1', [ + 'as' => 'user-delete', + 'filter' => 'permission:users-delete', + ]); + }); + }); // My account - $routes->get('my-account', 'Myaccount', [ - 'as' => 'myAccount', - ]); - $routes->get( - 'my-account/change-password', - 'Myaccount::changePassword/$1', - [ - 'as' => 'myAccount_change-password', - ] - ); - $routes->post( - 'my-account/change-password', - 'Myaccount::attemptChange/$1', - [ - 'as' => 'myAccount_change-password', - ] - ); + $routes->group('my-account', function ($routes) { + $routes->get('/', 'Myaccount', [ + 'as' => 'my-account', + ]); + $routes->get('change-password', 'Myaccount::changePassword/$1', [ + 'as' => 'change-password', + ]); + $routes->post('change-password', 'Myaccount::attemptChange/$1'); + }); } ); /** * Overwriting Myth:auth routes file */ -$routes->group( - config('App')->authGateway, - ['namespace' => 'Myth\Auth\Controllers'], - function ($routes) { - // Login/out - $routes->get('login', 'AuthController::login', ['as' => 'login']); - $routes->post('login', 'AuthController::attemptLogin'); - $routes->get('logout', 'AuthController::logout', ['as' => 'logout']); +$routes->group(config('App')->authGateway, function ($routes) { + // Login/out + $routes->get('login', 'Auth::login', ['as' => 'login']); + $routes->post('login', 'Auth::attemptLogin'); + $routes->get('logout', 'Auth::logout', ['as' => 'logout']); - // Registration - $routes->get('register', 'AuthController::register', [ - 'as' => 'register', - ]); - $routes->post('register', 'AuthController::attemptRegister'); + // Registration + $routes->get('register', 'Auth::register', [ + 'as' => 'register', + ]); + $routes->post('register', 'Auth::attemptRegister'); - // Activation - $routes->get('activate-account', 'AuthController::activateAccount', [ - 'as' => 'activate-account', - ]); - $routes->get( - 'resend-activate-account', - 'AuthController::resendActivateAccount', - [ - 'as' => 'resend-activate-account', - ] - ); + // Activation + $routes->get('activate-account', 'Auth::activateAccount', [ + 'as' => 'activate-account', + ]); + $routes->get('resend-activate-account', 'Auth::resendActivateAccount', [ + 'as' => 'resend-activate-account', + ]); - // Forgot/Resets - $routes->get('forgot', 'AuthController::forgotPassword', [ - 'as' => 'forgot', - ]); - $routes->post('forgot', 'AuthController::attemptForgot'); - $routes->get('reset-password', 'AuthController::resetPassword', [ - 'as' => 'reset-password', - ]); - $routes->post('reset-password', 'AuthController::attemptReset'); - } -); + // Forgot/Resets + $routes->get('forgot', 'Auth::forgotPassword', [ + 'as' => 'forgot', + ]); + $routes->post('forgot', 'Auth::attemptForgot'); + $routes->get('reset-password', 'Auth::resetPassword', [ + 'as' => 'reset-password', + ]); + $routes->post('reset-password', 'Auth::attemptReset'); +}); /** * -------------------------------------------------------------------- diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php index def6ab68..c892dc5c 100644 --- a/app/Controllers/Admin/Contributor.php +++ b/app/Controllers/Admin/Contributor.php @@ -70,10 +70,32 @@ class Contributor extends BaseController public function add() { + helper('form'); + + $users = (new UserModel())->findAll(); + $userOptions = array_reduce( + $users, + function ($result, $user) { + $result[$user->id] = $user->username; + return $result; + }, + [] + ); + + $roles = (new GroupModel())->getContributorRoles(); + $roleOptions = array_reduce( + $roles, + function ($result, $role) { + $result[$role->id] = lang('Contributor.roles.' . $role->name); + return $result; + }, + [] + ); + $data = [ 'podcast' => $this->podcast, - 'users' => (new UserModel())->findAll(), - 'roles' => (new GroupModel())->getContributorRoles(), + 'userOptions' => $userOptions, + 'roleOptions' => $roleOptions, ]; replace_breadcrumb_params([0 => $this->podcast->title]); @@ -97,11 +119,23 @@ class Contributor extends BaseController ]); } - return redirect()->route('contributor_list', [$this->podcast->id]); + return redirect()->route('contributor-list', [$this->podcast->id]); } public function edit() { + helper('form'); + + $roles = (new GroupModel())->getContributorRoles(); + $roleOptions = array_reduce( + $roles, + function ($result, $role) { + $result[$role->id] = lang('Contributor.roles.' . $role->name); + return $result; + }, + [] + ); + $data = [ 'podcast' => $this->podcast, 'user' => $this->user, @@ -109,7 +143,7 @@ class Contributor extends BaseController $this->user->id, $this->podcast->id ), - 'roles' => (new GroupModel())->getContributorRoles(), + 'roleOptions' => $roleOptions, ]; replace_breadcrumb_params([ @@ -127,7 +161,7 @@ class Contributor extends BaseController $this->request->getPost('role') ); - return redirect()->route('contributor_list', [$this->podcast->id]); + return redirect()->route('contributor-list', [$this->podcast->id]); } public function remove() diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index d1bc612c..32ca7f3a 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -86,6 +86,9 @@ class Episode extends BaseController 'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]', 'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', + 'publication_date' => 'valid_date[Y-m-d]|permit_empty', + 'publication_time' => + 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', ]; if (!$this->validate($rules)) { @@ -100,17 +103,20 @@ class Episode extends BaseController 'title' => $this->request->getPost('title'), 'slug' => $this->request->getPost('slug'), 'enclosure' => $this->request->getFile('enclosure'), - 'pub_date' => $this->request->getPost('pub_date'), 'description' => $this->request->getPost('description'), 'image' => $this->request->getFile('image'), - 'explicit' => (bool) $this->request->getPost('explicit'), + 'explicit' => $this->request->getPost('explicit') == 'yes', 'number' => $this->request->getPost('episode_number'), 'season_number' => $this->request->getPost('season_number'), 'type' => $this->request->getPost('type'), - 'author_name' => $this->request->getPost('author_name'), - 'author_email' => $this->request->getPost('author_email'), - 'block' => (bool) $this->request->getPost('block'), + 'block' => $this->request->getPost('block') == 'yes', + 'created_by' => user(), + 'updated_by' => user(), ]); + $newEpisode->setPublishedAt( + $this->request->getPost('publication_date'), + $this->request->getPost('publication_time') + ); $episodeModel = new EpisodeModel(); @@ -121,7 +127,7 @@ class Episode extends BaseController ->with('errors', $episodeModel->errors()); } - return redirect()->route('episode_list', [$this->podcast->id]); + return redirect()->route('episode-list', [$this->podcast->id]); } public function edit() @@ -146,6 +152,9 @@ class Episode extends BaseController 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty', 'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', + 'publication_date' => 'valid_date[Y-m-d]|permit_empty', + 'publication_time' => + 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', ]; if (!$this->validate($rules)) { @@ -157,17 +166,19 @@ class Episode extends BaseController $this->episode->title = $this->request->getPost('title'); $this->episode->slug = $this->request->getPost('slug'); - $this->episode->pub_date = $this->request->getPost('pub_date'); $this->episode->description = $this->request->getPost('description'); - $this->episode->explicit = (bool) $this->request->getPost('explicit'); + $this->episode->explicit = $this->request->getPost('explicit') == 'yes'; $this->episode->number = $this->request->getPost('episode_number'); $this->episode->season_number = $this->request->getPost('season_number') ? $this->request->getPost('season_number') : null; $this->episode->type = $this->request->getPost('type'); - $this->episode->author_name = $this->request->getPost('author_name'); - $this->episode->author_email = $this->request->getPost('author_email'); - $this->episode->block = (bool) $this->request->getPost('block'); + $this->episode->block = $this->request->getPost('block') == 'yes'; + $this->episode->setPublishedAt( + $this->request->getPost('publication_date'), + $this->request->getPost('publication_time') + ); + $this->episode->updated_by = user(); $enclosure = $this->request->getFile('enclosure'); if ($enclosure->isValid()) { @@ -187,13 +198,13 @@ class Episode extends BaseController ->with('errors', $episodeModel->errors()); } - return redirect()->route('episode_list', [$this->podcast->id]); + return redirect()->route('episode-list', [$this->podcast->id]); } public function delete() { (new EpisodeModel())->delete($this->episode->id); - return redirect()->route('episode_list', [$this->podcast->id]); + return redirect()->route('episode-list', [$this->podcast->id]); } } diff --git a/app/Controllers/Admin/Myaccount.php b/app/Controllers/Admin/Myaccount.php index fa7e7b4d..b6d19117 100644 --- a/app/Controllers/Admin/Myaccount.php +++ b/app/Controllers/Admin/Myaccount.php @@ -20,6 +20,8 @@ class Myaccount extends BaseController public function changePassword() { + helper('form'); + return view('admin/my_account/change_password'); } @@ -31,10 +33,8 @@ class Myaccount extends BaseController // Validate here first, since some things, // like the password, can only be validated properly here. $rules = [ - 'email' => 'required|valid_email', 'password' => 'required', - 'new_password' => 'required|strong_password', - 'new_pass_confirm' => 'required|matches[new_password]', + 'new_password' => 'required|strong_password|differs[password]', ]; if (!$this->validate($rules)) { @@ -53,7 +53,7 @@ class Myaccount extends BaseController return redirect() ->back() ->withInput() - ->with('errors', $userModel->errors()); + ->with('error', lang('MyAccount.messages.wrongPasswordError')); } user()->password = $this->request->getPost('new_password'); @@ -68,7 +68,7 @@ class Myaccount extends BaseController // Success! return redirect() - ->route('myAccount') + ->back() ->with('message', lang('MyAccount.messages.passwordChangeSuccess')); } } diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 3b18f44b..2f9dcef1 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -43,7 +43,7 @@ class Podcast extends BaseController public function list() { if (!has_permission('podcasts-list')) { - return redirect()->route('my_podcasts'); + return redirect()->route('my-podcasts'); } $data = ['podcasts' => (new PodcastModel())->findAll()]; @@ -63,11 +63,30 @@ class Podcast extends BaseController { helper(['form', 'misc']); - $languageModel = new LanguageModel(); - $categoryModel = new CategoryModel(); + $categories = (new CategoryModel())->findAll(); + $languages = (new LanguageModel())->findAll(); + $languageOptions = array_reduce( + $languages, + function ($result, $language) { + $result[$language->code] = $language->native_name; + return $result; + }, + [] + ); + $categoryOptions = array_reduce( + $categories, + function ($result, $category) { + $result[$category->code] = lang( + 'Podcast.category_options.' . $category->code + ); + return $result; + }, + [] + ); + $data = [ - 'languages' => $languageModel->findAll(), - 'categories' => $categoryModel->findAll(), + 'languageOptions' => $languageOptions, + 'categoryOptions' => $categoryOptions, 'browserLang' => get_browser_language( $this->request->getServer('HTTP_ACCEPT_LANGUAGE') ), @@ -99,17 +118,17 @@ class Podcast extends BaseController 'image' => $this->request->getFile('image'), 'language' => $this->request->getPost('language'), 'category' => $this->request->getPost('category'), - 'explicit' => (bool) $this->request->getPost('explicit'), - 'author_name' => $this->request->getPost('author_name'), - 'author_email' => $this->request->getPost('author_email'), - 'owner' => user(), + 'explicit' => $this->request->getPost('explicit') == 'yes', + 'author' => $this->request->getPost('author'), 'owner_name' => $this->request->getPost('owner_name'), 'owner_email' => $this->request->getPost('owner_email'), 'type' => $this->request->getPost('type'), 'copyright' => $this->request->getPost('copyright'), - 'block' => (bool) $this->request->getPost('block'), - 'complete' => (bool) $this->request->getPost('complete'), + 'block' => $this->request->getPost('block') == 'yes', + 'complete' => $this->request->getPost('complete') == 'yes', 'custom_html_head' => $this->request->getPost('custom_html_head'), + 'created_by' => user(), + 'updated_by' => user(), ]); $podcastModel = new PodcastModel(); @@ -136,17 +155,38 @@ class Podcast extends BaseController $db->transComplete(); - return redirect()->route('podcast_list'); + return redirect()->route('podcast-list'); } public function edit() { helper('form'); + $categories = (new CategoryModel())->findAll(); + $languages = (new LanguageModel())->findAll(); + $languageOptions = array_reduce( + $languages, + function ($result, $language) { + $result[$language->code] = $language->native_name; + return $result; + }, + [] + ); + $categoryOptions = array_reduce( + $categories, + function ($result, $category) { + $result[$category->code] = lang( + 'Podcast.category_options.' . $category->code + ); + return $result; + }, + [] + ); + $data = [ 'podcast' => $this->podcast, - 'languages' => (new LanguageModel())->findAll(), - 'categories' => (new CategoryModel())->findAll(), + 'languageOptions' => $languageOptions, + 'categoryOptions' => $categoryOptions, ]; replace_breadcrumb_params([0 => $this->podcast->title]); @@ -180,18 +220,18 @@ class Podcast extends BaseController } $this->podcast->language = $this->request->getPost('language'); $this->podcast->category = $this->request->getPost('category'); - $this->podcast->explicit = (bool) $this->request->getPost('explicit'); - $this->podcast->author_name = $this->request->getPost('author_name'); - $this->podcast->author_email = $this->request->getPost('author_email'); + $this->podcast->explicit = $this->request->getPost('explicit') == 'yes'; + $this->podcast->author = $this->request->getPost('author'); $this->podcast->owner_name = $this->request->getPost('owner_name'); $this->podcast->owner_email = $this->request->getPost('owner_email'); $this->podcast->type = $this->request->getPost('type'); $this->podcast->copyright = $this->request->getPost('copyright'); - $this->podcast->block = (bool) $this->request->getPost('block'); - $this->podcast->complete = (bool) $this->request->getPost('complete'); + $this->podcast->block = $this->request->getPost('block') == 'yes'; + $this->podcast->complete = $this->request->getPost('complete') == 'yes'; $this->podcast->custom_html_head = $this->request->getPost( 'custom_html_head' ); + $this->updated_by = user(); $podcastModel = new PodcastModel(); @@ -202,13 +242,13 @@ class Podcast extends BaseController ->with('errors', $podcastModel->errors()); } - return redirect()->route('podcast_list'); + return redirect()->route('podcast-list'); } public function delete() { (new PodcastModel())->delete($this->podcast->id); - return redirect()->route('podcast_list'); + return redirect()->route('podcast-list'); } } diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php index 52528c67..63874156 100644 --- a/app/Controllers/Admin/User.php +++ b/app/Controllers/Admin/User.php @@ -47,6 +47,8 @@ class User extends BaseController public function create() { + helper('form'); + $data = [ 'roles' => (new GroupModel())->getUserRoles(), ]; @@ -65,7 +67,6 @@ class User extends BaseController [ 'email' => 'required|valid_email|is_unique[users.email]', 'password' => 'required|strong_password', - 'pass_confirm' => 'required|matches[password]', ] ); @@ -94,7 +95,7 @@ class User extends BaseController // Success! return redirect() - ->route('user_list') + ->route('user-list') ->with( 'message', lang('User.messages.createSuccess', [ @@ -105,9 +106,21 @@ class User extends BaseController public function edit() { + helper('form'); + + $roles = (new GroupModel())->getUserRoles(); + $roleOptions = array_reduce( + $roles, + function ($result, $role) { + $result[$role->name] = lang('User.roles.' . $role->name); + return $result; + }, + [] + ); + $data = [ 'user' => $this->user, - 'roles' => (new GroupModel())->getUserRoles(), + 'roleOptions' => $roleOptions, ]; replace_breadcrumb_params([0 => $this->user->username]); @@ -123,7 +136,7 @@ class User extends BaseController // Success! return redirect() - ->route('user_list') + ->route('user-list') ->with( 'message', lang('User.messages.rolesEditSuccess', [ @@ -145,7 +158,7 @@ class User extends BaseController // Success! return redirect() - ->route('user_list') + ->route('user-list') ->with( 'message', lang('User.messages.forcePassResetSuccess', [ @@ -178,7 +191,7 @@ class User extends BaseController } return redirect() - ->route('user_list') + ->route('user-list') ->with( 'message', lang('User.messages.banSuccess', [ @@ -199,7 +212,7 @@ class User extends BaseController } return redirect() - ->route('user_list') + ->route('user-list') ->with( 'message', lang('User.messages.unbanSuccess', [ diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php new file mode 100644 index 00000000..aaac73cc --- /dev/null +++ b/app/Controllers/Auth.php @@ -0,0 +1,167 @@ +config->allowRegistration) { + return redirect() + ->back() + ->withInput() + ->with('error', lang('Auth.registerDisabled')); + } + + $users = model('UserModel'); + + // Validate here first, since some things, + // like the password, can only be validated properly here. + $rules = [ + 'username' => + 'required|alpha_numeric_space|min_length[3]|is_unique[users.username]', + 'email' => 'required|valid_email|is_unique[users.email]', + 'password' => 'required|strong_password', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', service('validation')->getErrors()); + } + + // Save the user + $allowedPostFields = array_merge( + ['password'], + $this->config->validFields, + $this->config->personalFields + ); + $user = new User($this->request->getPost($allowedPostFields)); + + $this->config->requireActivation !== false + ? $user->generateActivateHash() + : $user->activate(); + + // Ensure default group gets assigned if set + if (!empty($this->config->defaultUserGroup)) { + $users = $users->withGroup($this->config->defaultUserGroup); + } + + if (!$users->save($user)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $users->errors()); + } + + if ($this->config->requireActivation !== false) { + $activator = service('activator'); + $sent = $activator->send($user); + + if (!$sent) { + return redirect() + ->back() + ->withInput() + ->with( + 'error', + $activator->error() ?? lang('Auth.unknownError') + ); + } + + // Success! + return redirect() + ->route('login') + ->with('message', lang('Auth.activationSuccess')); + } + + // Success! + return redirect() + ->route('login') + ->with('message', lang('Auth.registerSuccess')); + } + + /** + * Verifies the code with the email and saves the new password, + * if they all pass validation. + * + * @return mixed + */ + public function attemptReset() + { + if ($this->config->activeResetter === false) { + return redirect() + ->route('login') + ->with('error', lang('Auth.forgotDisabled')); + } + + $users = model('UserModel'); + + // First things first - log the reset attempt. + $users->logResetAttempt( + $this->request->getPost('email'), + $this->request->getPost('token'), + $this->request->getIPAddress(), + (string) $this->request->getUserAgent() + ); + + $rules = [ + 'token' => 'required', + 'email' => 'required|valid_email', + 'password' => 'required|strong_password', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $users->errors()); + } + + $user = $users + ->where('email', $this->request->getPost('email')) + ->where('reset_hash', $this->request->getPost('token')) + ->first(); + + if (is_null($user)) { + return redirect() + ->back() + ->with('error', lang('Auth.forgotNoUser')); + } + + // Reset token still valid? + if ( + !empty($user->reset_expires) && + time() > $user->reset_expires->getTimestamp() + ) { + return redirect() + ->back() + ->withInput() + ->with('error', lang('Auth.resetTokenExpired')); + } + + // Success! Save the new password, and cleanup the reset hash. + $user->password = $this->request->getPost('password'); + $user->reset_hash = null; + $user->reset_at = date('Y-m-d H:i:s'); + $user->reset_expires = null; + $user->force_pass_reset = false; + $users->save($user); + + return redirect() + ->route('login') + ->with('message', lang('Auth.resetSuccess')); + } +} diff --git a/app/Database/Migrations/2020-05-30-101000_add_languages.php b/app/Database/Migrations/2020-05-30-101000_add_languages.php index 95c817ff..ae11bdf7 100644 --- a/app/Database/Migrations/2020-05-30-101000_add_languages.php +++ b/app/Database/Migrations/2020-05-30-101000_add_languages.php @@ -30,7 +30,6 @@ class AddLanguages extends Migration ], 'native_name' => [ 'type' => 'VARCHAR', - 'comment' => 'Native language name.', 'constraint' => 191, ], ]); diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php index 373345dc..368699c1 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -23,125 +23,90 @@ class AddPodcasts extends Migration 'constraint' => 20, 'unsigned' => true, 'auto_increment' => true, - 'comment' => 'The podcast ID', ], 'title' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The show title. It’s important to have a clear, concise name for your podcast. Make your title specific. A show titled Our Community Bulletin is too vague to attract many subscribers, no matter how compelling the content. Pay close attention to the title as Apple Podcasts uses this field for search. If you include a long list of keywords in an attempt to game podcast search, your show may be removed from the Apple directory.', ], 'name' => [ 'type' => 'VARCHAR', 'constraint' => 191, 'unique' => true, - 'comment' => 'Unique podcast string identifier.', ], 'description' => [ 'type' => 'TEXT', - 'comment' => - 'The show description. Where description is text containing one or more sentences describing your podcast to potential listeners. The maximum amount of text allowed for this tag is 4000 characters. To include links in your description or rich HTML, adhere to the following technical guidelines: enclose all portions of your XML that contain embedded HTML in a CDATA section to prevent formatting issues, and to ensure proper link functionality.', ], 'image_uri' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The artwork for the show. Specify your show artwork by providing a URL linking to it. Depending on their device, subscribers see your podcast artwork in varying sizes. Therefore, make sure your design is effective at both its original size and at thumbnail size. You should include a show title, brand, or source name as part of your podcast artwork. Artwork must be a minimum size of 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate file extensions (.jpg, .png), and in the RGB colorspace.', ], 'language' => [ 'type' => 'VARCHAR', 'constraint' => 2, - 'comment' => - 'The language spoken on the show. Because Apple Podcasts is available in territories around the world, it is critical to specify the language of a podcast. Apple Podcasts only supports values from the ISO 639 list (two-letter language codes, with some possible modifiers, such as "en-us"). Invalid language codes will cause your feed to fail Apple validation.', ], 'category' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The show category information. For a complete list of categories and subcategories, see Apple Podcasts categories. Select the category that best reflects the content of your show. If available, you can also define a subcategory. Although you can specify more than one category and subcategory in your RSS feed, Apple Podcasts only recognizes the first category and subcategory. When specifying categories and subcategories, be sure to properly escape ampersands.', 'null' => true, ], 'explicit' => [ 'type' => 'TINYINT', 'constraint' => 1, 'default' => 0, - 'comment' => - 'The podcast parental advisory information. The explicit value can be one of the following: True: If you specify true, indicating the presence of explicit content, Apple Podcasts displays an Explicit parental advisory graphic for your podcast. Podcasts containing explicit material aren’t available in some Apple Podcasts territories. False: If you specify false, indicating that your podcast doesn’t contain explicit language or adult content, Apple Podcasts displays a Clean parental advisory graphic for your podcast.', ], - 'author_name' => [ + 'author' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'Name of the group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.', 'null' => true, ], - 'author_email' => [ - 'type' => 'VARCHAR', - 'constraint' => 1024, - 'owner_email' => - 'Email of the group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.', - 'null' => true, - ], - 'owner_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'comment' => 'The podcast owner.', - ], 'owner_name' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The podcast owner name. Note: The owner information is for administrative communication about the podcast and isn’t displayed in Apple Podcasts.', 'null' => true, ], 'owner_email' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The podcast owner email address. Note: The owner information is for administrative communication about the podcast and isn’t displayed in Apple Podcasts. Please make sure the email address is active and monitored.', 'null' => true, ], 'type' => [ 'type' => 'ENUM', 'constraint' => ['episodic', 'serial'], 'default' => 'episodic', - 'comment' => - 'The type of show. If your show is Serial you must use this tag. Its values can be one of the following: episodic (default). Specify episodic when episodes are intended to be consumed without any specific order. Apple Podcasts will present newest episodes first and display the publish date (required) of each episode. If organized into seasons, the newest season will be presented first - otherwise, episodes will be grouped by year published, newest first. For new subscribers, Apple Podcasts adds the newest, most recent episode in their Library. serial. Specify serial when episodes are intended to be consumed in sequential order. Apple Podcasts will present the oldest episodes first and display the episode numbers (required) of each episode. If organized into seasons, the newest season will be presented first and numbers must be given for each episode. For new subscribers, Apple Podcasts adds the first episode to their Library, or the entire current season if using seasons', ], 'copyright' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The show copyright details. If your show is copyrighted you should use this tag.', 'null' => true, ], 'block' => [ 'type' => 'TINYINT', 'constraint' => 1, 'default' => 0, - 'comment' => - 'The podcast show or hide status. If you want your show removed from the Apple directory, use this tag. Specifying the tag with a Yes value, prevents the entire podcast from appearing in Apple Podcasts. Specifying any value other than Yes has no effect.', ], 'complete' => [ 'type' => 'TINYINT', 'constraint' => 1, 'default' => 0, - 'comment' => - 'The podcast update status. If you will never publish another episode to your show, use this tag. Specifying the tag with a Yes value indicates that a podcast is complete and you will not post any more episodes in the future. Specifying any value other than Yes has no effect.', ], 'episode_description_footer' => [ 'type' => 'TEXT', - 'comment' => - 'The text that will be added in every episode description (show notes).', 'null' => true, ], 'custom_html_head' => [ 'type' => 'TEXT', - 'comment' => - 'The HTML code that will be added to every page for this podcast. (You could add Google Analytics tracking code here for instance.)', 'null' => true, ], + 'created_by' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + ], + 'updated_by' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + ], 'created_at' => [ 'type' => 'TIMESTAMP', ], @@ -154,7 +119,8 @@ class AddPodcasts extends Migration ], ]); $this->forge->addKey('id', true); - $this->forge->addForeignKey('owner_id', 'users', 'id'); + $this->forge->addForeignKey('created_by', 'users', 'id'); + $this->forge->addForeignKey('updated_by', 'users', 'id'); $this->forge->createTable('podcasts'); } diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index b29f0583..23bd03ff 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -23,98 +23,73 @@ class AddEpisodes extends Migration 'constraint' => 20, 'unsigned' => true, 'auto_increment' => true, - 'comment' => 'The episode ID', ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, - 'comment' => 'The podcast ID', ], 'title' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'An episode title. title is a string containing a clear, concise name for your episode. Don’t specify the episode number or season number in this tag.', ], 'slug' => [ 'type' => 'VARCHAR', 'constraint' => 191, - 'comment' => 'Episode slug for URLs', ], 'enclosure_uri' => [ 'type' => 'VARCHAR', 'constraint' => 1024, - 'comment' => - 'The URI attribute points to your podcast media file. The file extension specified within the URI attribute determines whether or not content appears in the podcast directory. Supported file formats include M4A, MP3, MOV, MP4, M4V, and PDF.', - ], - 'pub_date' => [ - 'type' => 'DATETIME', - 'comment' => - 'The date and time when an episode was released. Format the date using the RFC 2822 specifications. For example: Wed, 15 Jun 2019 19:00:00 UTC.', ], + 'description' => [ 'type' => 'TEXT', 'null' => true, - 'comment' => - 'An episode description. Description is text containing one or more sentences describing your episode to potential listeners. You can specify up to 4000 characters. You can use rich text formatting and some HTML (

,

    ,