diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 494a3f62c086dde7a343f42ee5767ac4d4c6d080..640d7250d0e1b30ae221dc6461e0f491a66ec86b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -36,11 +36,12 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}'); // route since we don't have to scan directories. $routes->get('/', 'Home::index', ['as' => 'home']); +// Public routes $routes->group('@(:podcastName)', function ($routes) { - $routes->add('/', 'Podcast/$1', ['as' => 'podcast']); + $routes->get('/', 'Podcast/$1', ['as' => 'podcast']); - $routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); - $routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [ + $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); + $routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [ 'as' => 'episode', ]); }); @@ -51,74 +52,119 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [ ]); // Show the Unknown UserAgents -$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents'); -$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1'); +$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents'); +$routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1'); // Admin area $routes->group( config('App')->adminGateway, ['namespace' => 'App\Controllers\Admin'], function ($routes) { - $routes->add('/', 'Home', [ + $routes->get('/', 'Home', [ 'as' => 'admin', ]); - $routes->add('new-podcast', 'Podcast::create', [ + $routes->get('my-podcasts', 'Podcast::myPodcasts', [ + 'as' => 'my_podcasts', + ]); + $routes->get('podcasts', 'Podcast::list', [ + 'as' => 'podcast_list', + 'filter' => 'permission:podcasts-list', + ]); + $routes->get('new-podcast', 'Podcast::create', [ 'as' => 'podcast_create', + 'filter' => 'permission:podcasts-create', + ]); + $routes->post('new-podcast', 'Podcast::attemptCreate', [ + 'filter' => 'permission:podcasts-create', ]); - $routes->add('podcasts', 'Podcast::list', ['as' => 'podcast_list']); - $routes->group('podcasts/@(:podcastName)', function ($routes) { - $routes->add('edit', 'Podcast::edit/$1', [ + // Use ids in admin area to help permission and group lookups + $routes->group('podcasts/(:num)', function ($routes) { + $routes->get('edit', 'Podcast::edit/$1', [ 'as' => 'podcast_edit', ]); + $routes->post('edit', 'Podcast::attemptEdit/$1'); $routes->add('delete', 'Podcast::delete/$1', [ 'as' => 'podcast_delete', ]); - $routes->add('new-episode', 'Episode::create/$1', [ + // Podcast episodes + $routes->get('episodes', 'Episode::list/$1', [ + 'as' => 'episode_list', + ]); + $routes->get('new-episode', 'Episode::create/$1', [ 'as' => 'episode_create', ]); - $routes->add('episodes', 'Episode::list/$1', [ - 'as' => 'episode_list', + $routes->post('new-episode', 'Episode::attemptCreate/$1'); + + $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [ + 'as' => 'episode_edit', + ]); + $routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2'); + $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [ + 'as' => 'episode_delete', ]); - $routes->add( - 'episodes/(:episodeSlug)/edit', - 'Episode::edit/$1/$2', + // Podcast contributors + $routes->get('contributors', 'Contributor::list/$1', [ + 'as' => 'contributor_list', + ]); + $routes->get('add-contributor', 'Contributor::add/$1', [ + 'as' => 'contributor_add', + ]); + $routes->post('add-contributor', 'Contributor::attemptAdd/$1'); + $routes->get( + 'contributors/(:num)/edit', + 'Contributor::edit/$1/$2', [ - 'as' => 'episode_edit', + 'as' => 'contributor_edit', ] ); + $routes->post( + 'contributors/(:num)/edit', + 'Contributor::attemptEdit/$1/$2' + ); $routes->add( - 'episodes/(:episodeSlug)/delete', - 'Episode::delete/$1/$2', - [ - 'as' => 'episode_delete', - ] + 'contributors/(:num)/remove', + 'Contributor::remove/$1/$2', + ['as' => 'contributor_remove'] ); }); // Users - $routes->add('users', 'User::list', ['as' => 'user_list']); - $routes->add('new-user', 'User::create', ['as' => 'user_create']); + $routes->get('users', 'User::list', [ + 'as' => 'user_list', + 'filter' => 'permission:users-list', + ]); + $routes->get('new-user', 'User::create', [ + 'as' => 'user_create', + 'filter' => 'permission:users-create', + ]); + $routes->post('new-user', 'User::attemptCreate', [ + 'filter' => 'permission:users-create', + ]); - $routes->add('users/@(:any)/ban', 'User::ban/$1', [ + $routes->add('users/(:num)/ban', 'User::ban/$1', [ 'as' => 'user_ban', + 'filter' => 'permission:users-manage_bans', ]); - $routes->add('users/@(:any)/unban', 'User::unBan/$1', [ + $routes->add('users/(:num)/unban', 'User::unBan/$1', [ 'as' => 'user_unban', + 'filter' => 'permission:users-manage_bans', ]); $routes->add( - 'users/@(:any)/force-pass-reset', + 'users/(:num)/force-pass-reset', 'User::forcePassReset/$1', [ 'as' => 'user_force_pass_reset', + 'filter' => 'permission:users-force_pass_reset', ] ); - $routes->add('users/@(:any)/delete', 'User::delete/$1', [ + $routes->add('users/(:num)/delete', 'User::delete/$1', [ 'as' => 'user_delete', + 'filter' => 'permission:users-delete', ]); // My account diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php new file mode 100644 index 0000000000000000000000000000000000000000..1cf39aee2ae21de24575e26115c2b68634b1e64b --- /dev/null +++ b/app/Controllers/Admin/Contributor.php @@ -0,0 +1,187 @@ +<?php +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Controllers\Admin; + +use App\Models\PodcastModel; +use Myth\Auth\Authorization\GroupModel; +use Myth\Auth\Config\Services; +use Myth\Auth\Models\UserModel; + +class Contributor extends BaseController +{ + protected \App\Entities\Podcast $podcast; + protected ?\Myth\Auth\Entities\User $user; + + public function _remap($method, ...$params) + { + if ( + !has_permission('podcasts-manage_contributors') || + !has_permission("podcasts:$params[0]-manage_contributors") + ) { + throw new \RuntimeException(lang('Auth.notEnoughPrivilege')); + } + + $podcast_model = new PodcastModel(); + + $this->podcast = $podcast_model->find($params[0]); + + if (count($params) > 1) { + $user_model = new UserModel(); + if ( + !($this->user = $user_model + ->select('users.*') + ->join( + 'users_podcasts', + 'users_podcasts.user_id = users.id' + ) + ->where([ + 'users.id' => $params[1], + 'podcast_id' => $params[0], + ]) + ->first()) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + } + + return $this->$method(); + } + + public function list() + { + $data = [ + 'podcast' => $this->podcast, + ]; + + echo view('admin/contributor/list', $data); + } + + public function add() + { + $user_model = new UserModel(); + $group_model = new GroupModel(); + + $roles = $group_model + ->select('auth_groups.*') + ->like('name', 'podcasts:' . $this->podcast->id, 'after') + ->findAll(); + + $data = [ + 'podcast' => $this->podcast, + 'users' => $user_model->findAll(), + 'roles' => $roles, + ]; + + echo view('admin/contributor/add', $data); + } + + public function attemptAdd() + { + $authorize = Services::authorization(); + + $user_id = (int) $this->request->getPost('user'); + $group_id = (int) $this->request->getPost('role'); + + // Add user to chosen group + $authorize->addUserToGroup($user_id, $group_id); + + (new PodcastModel())->addContributorToPodcast( + $user_id, + $this->podcast->id + ); + + return redirect()->route('contributor_list', [$this->podcast->id]); + } + + public function edit() + { + $group_model = new GroupModel(); + + $roles = $group_model + ->select('auth_groups.*') + ->like('name', 'podcasts:' . $this->podcast->id, 'after') + ->findAll(); + + $user_role = $group_model + ->select('auth_groups.*') + ->join( + 'auth_groups_users', + 'auth_groups_users.group_id = auth_groups.id' + ) + ->where('auth_groups_users.user_id', $this->user->id) + ->like('name', 'podcasts:' . $this->podcast->id, 'after') + ->first(); + + $data = [ + 'podcast' => $this->podcast, + 'user' => $this->user, + 'user_role' => $user_role, + 'roles' => $roles, + ]; + + echo view('admin/contributor/edit', $data); + } + + public function attemptEdit() + { + $authorize = Services::authorization(); + + $group_model = new GroupModel(); + + $group = $group_model + ->select('auth_groups.*') + ->join( + 'auth_groups_users', + 'auth_groups_users.group_id = auth_groups.id' + ) + ->where('user_id', $this->user->id) + ->like('name', 'podcasts:' . $this->podcast->id, 'after') + ->first(); + + $authorize->removeUserFromGroup( + (int) $this->user->id, + (int) $group->id + ); + + $authorize->addUserToGroup( + (int) $this->user->id, + (int) $this->request->getPost('role') + ); + + return redirect()->route('contributor_list', [$this->podcast->id]); + } + + public function remove() + { + $authorize = Services::authorization(); + + $group_model = new GroupModel(); + + $group = $group_model + ->select('auth_groups.*') + ->join( + 'auth_groups_users', + 'auth_groups_users.group_id = auth_groups.id' + ) + ->like('name', 'podcasts:' . $this->podcast->id, 'after') + ->where('user_id', $this->user->id) + ->first(); + + $authorize->removeUserFromGroup( + (int) $this->user->id, + (int) $group->id + ); + + (new PodcastModel())->removeContributorFromPodcast( + $this->user->id, + $this->podcast->id + ); + + return redirect()->route('contributor_list', [$this->podcast->id]); + } +} diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index 105aed974e202994e1a5bb5aacac7508fbf592da..2dc9aabc2edf425d1f452fe7441d99c713552497 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -17,23 +17,52 @@ class Episode extends BaseController public function _remap($method, ...$params) { + switch ($method) { + case 'list': + if ( + !has_permission('episodes-list') || + !has_permission("podcasts:$params[0]:episodes-list") + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + case 'edit': + if ( + !has_permission('episodes-edit') || + !has_permission("podcasts:$params[0]:episodes-edit") + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + case 'delete': + if ( + !has_permission('episodes-delete') || + !has_permission("podcasts:$params[0]:episodes-delete") + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + } + $podcast_model = new PodcastModel(); - $this->podcast = $podcast_model->where('name', $params[0])->first(); + $this->podcast = $podcast_model->find($params[0]); if (count($params) > 1) { $episode_model = new EpisodeModel(); if ( - !($episode = $episode_model + !($this->episode = $episode_model ->where([ - 'podcast_id' => $this->podcast->id, - 'slug' => $params[1], + 'id' => $params[1], + 'podcast_id' => $params[0], ]) ->first()) ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - $this->episode = $episode; } return $this->$method(); @@ -41,13 +70,8 @@ class Episode extends BaseController public function list() { - $episode_model = new EpisodeModel(); - $data = [ 'podcast' => $this->podcast, - 'all_podcast_episodes' => $episode_model - ->where('podcast_id', $this->podcast->id) - ->find(), ]; return view('admin/episode/list', $data); @@ -57,105 +81,118 @@ class Episode extends BaseController { helper(['form']); - if ( - !$this->validate([ - 'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]', - 'image' => - 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', - 'title' => 'required', - 'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]', - 'description' => 'required', - 'type' => 'required', - ]) - ) { - $data = [ - 'podcast' => $this->podcast, - ]; - - echo view('admin/episode/create', $data); - } else { - $new_episode = new \App\Entities\Episode([ - 'podcast_id' => $this->podcast->id, - 'title' => $this->request->getVar('title'), - 'slug' => $this->request->getVar('slug'), - 'enclosure' => $this->request->getFile('enclosure'), - 'pub_date' => $this->request->getVar('pub_date'), - 'description' => $this->request->getVar('description'), - 'image' => $this->request->getFile('image'), - 'explicit' => $this->request->getVar('explicit') or false, - 'number' => $this->request->getVar('episode_number'), - 'season_number' => $this->request->getVar('season_number'), - 'type' => $this->request->getVar('type'), - 'author_name' => $this->request->getVar('author_name'), - 'author_email' => $this->request->getVar('author_email'), - 'block' => $this->request->getVar('block') or false, - ]); + $data = [ + 'podcast' => $this->podcast, + ]; - $episode_model = new EpisodeModel(); - $episode_model->save($new_episode); + echo view('admin/episode/create', $data); + } - return redirect()->route('episode_list', [$this->podcast->name]); + public function attemptCreate() + { + $rules = [ + 'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]', + 'image' => + 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); } + + $new_episode = new \App\Entities\Episode([ + 'podcast_id' => $this->podcast->id, + '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'), + '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'), + ]); + + $episode_model = new EpisodeModel(); + + if (!$episode_model->save($new_episode)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $episode_model->errors()); + } + + return redirect()->route('episode_list', [$this->podcast->id]); } public function edit() { helper(['form']); - if ( - !$this->validate([ - 'enclosure' => - 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty', - 'image' => - 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', - 'title' => 'required', - 'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]', - 'description' => 'required', - 'type' => 'required', - ]) - ) { - $data = [ - 'podcast' => $this->podcast, - 'episode' => $this->episode, - ]; - - echo view('admin/episode/edit', $data); - } else { - $this->episode->title = $this->request->getVar('title'); - $this->episode->slug = $this->request->getVar('slug'); - $this->episode->pub_date = $this->request->getVar('pub_date'); - $this->episode->description = $this->request->getVar('description'); - $this->episode->explicit = - ($this->request->getVar('explicit') or false); - $this->episode->number = $this->request->getVar('episode_number'); - $this->episode->season_number = $this->request->getVar( - 'season_number' - ) - ? $this->request->getVar('season_number') - : null; - $this->episode->type = $this->request->getVar('type'); - $this->episode->author_name = $this->request->getVar('author_name'); - $this->episode->author_email = $this->request->getVar( - 'author_email' - ); - $this->episode->block = ($this->request->getVar('block') or false); - - $enclosure = $this->request->getFile('enclosure'); - if ($enclosure->isValid()) { - $this->episode->enclosure = $this->request->getFile( - 'enclosure' - ); - } - $image = $this->request->getFile('image'); - if ($image) { - $this->episode->image = $this->request->getFile('image'); - } + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + ]; - $episode_model = new EpisodeModel(); - $episode_model->save($this->episode); + echo view('admin/episode/edit', $data); + } + + public function attemptEdit() + { + $rules = [ + 'enclosure' => + 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty', + 'image' => + 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } - return redirect()->route('episode_list', [$this->podcast->name]); + $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->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'); + + $enclosure = $this->request->getFile('enclosure'); + if ($enclosure->isValid()) { + $this->episode->enclosure = $enclosure; } + $image = $this->request->getFile('image'); + if ($image) { + $this->episode->image = $image; + } + + $episode_model = new EpisodeModel(); + + if (!$episode_model->save($this->episode)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $episode_model->errors()); + } + + return redirect()->route('episode_list', [$this->podcast->id]); } public function delete() @@ -163,6 +200,6 @@ class Episode extends BaseController $episode_model = new EpisodeModel(); $episode_model->delete($this->episode->id); - return redirect()->route('episode_list', [$this->podcast->name]); + return redirect()->route('episode_list', [$this->podcast->id]); } } diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 502f15322ade638c1486d3a91f1a69e5f24b265c..3fa3110b4e7f7107f36cfd29947d8033fbeab737 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -6,7 +6,6 @@ */ namespace App\Controllers\Admin; -use App\Entities\UserPodcast; use App\Models\CategoryModel; use App\Models\LanguageModel; use App\Models\PodcastModel; @@ -18,18 +17,59 @@ class Podcast extends BaseController public function _remap($method, ...$params) { if (count($params) > 0) { + switch ($method) { + case 'edit': + if ( + !has_permission('podcasts-edit') || + !has_permission("podcasts:$params[0]-edit") + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + case 'delete': + if ( + !has_permission('podcasts-delete') || + !has_permission("podcasts:$params[0]-delete") + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + case 'listContributors': + case 'addContributor': + case 'editContributor': + case 'deleteContributor': + if ( + !has_permission('podcasts-manage_contributors') || + !has_permission( + "podcasts:$params[0]-manage_contributors" + ) + ) { + throw new \RuntimeException( + lang('Auth.notEnoughPrivilege') + ); + } + } + $podcast_model = new PodcastModel(); - if ( - !($podcast = $podcast_model->where('name', $params[0])->first()) - ) { + if (!($this->podcast = $podcast_model->find($params[0]))) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - $this->podcast = $podcast; } return $this->$method(); } + public function myPodcasts() + { + $data = [ + 'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id), + ]; + + return view('admin/podcast/list', $data); + } + public function list() { $podcast_model = new PodcastModel(); @@ -42,133 +82,141 @@ class Podcast extends BaseController public function create() { helper(['form', 'misc']); + + $languageModel = new LanguageModel(); + $categoryModel = new CategoryModel(); + $data = [ + 'languages' => $languageModel->findAll(), + 'categories' => $categoryModel->findAll(), + 'browser_lang' => get_browser_language( + $this->request->getServer('HTTP_ACCEPT_LANGUAGE') + ), + ]; + + echo view('admin/podcast/create', $data); + } + + public function attemptCreate() + { + $rules = [ + 'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + $podcast = new \App\Entities\Podcast([ + 'title' => $this->request->getPost('title'), + 'name' => $this->request->getPost('name'), + 'description' => $this->request->getPost('description'), + 'episode_description_footer' => $this->request->getPost( + 'episode_description_footer' + ), + '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(), + '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'), + 'custom_html_head' => $this->request->getPost('custom_html_head'), + ]); + $podcast_model = new PodcastModel(); + $db = \Config\Database::connect(); - if ( - !$this->validate([ - 'title' => 'required', - 'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]', - 'description' => 'required|max_length[4000]', - 'image' => - 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]', - 'owner_email' => 'required|valid_email', - 'type' => 'required', - ]) - ) { - $languageModel = new LanguageModel(); - $categoryModel = new CategoryModel(); - $data = [ - 'languages' => $languageModel->findAll(), - 'categories' => $categoryModel->findAll(), - 'browser_lang' => get_browser_language( - $this->request->getServer('HTTP_ACCEPT_LANGUAGE') - ), - ]; - - echo view('admin/podcast/create', $data); - } else { - $podcast = new \App\Entities\Podcast([ - 'title' => $this->request->getVar('title'), - 'name' => $this->request->getVar('name'), - 'description' => $this->request->getVar('description'), - 'episode_description_footer' => $this->request->getVar( - 'episode_description_footer' - ), - 'image' => $this->request->getFile('image'), - 'language' => $this->request->getVar('language'), - 'category' => $this->request->getVar('category'), - 'explicit' => $this->request->getVar('explicit') or false, - 'author_name' => $this->request->getVar('author_name'), - 'author_email' => $this->request->getVar('author_email'), - 'owner_name' => $this->request->getVar('owner_name'), - 'owner_email' => $this->request->getVar('owner_email'), - 'type' => $this->request->getVar('type'), - 'copyright' => $this->request->getVar('copyright'), - 'block' => $this->request->getVar('block') or false, - 'complete' => $this->request->getVar('complete') or false, - 'custom_html_head' => $this->request->getVar( - 'custom_html_head' - ), - ]); - - $db = \Config\Database::connect(); - - $db->transStart(); - - $new_podcast_id = $podcast_model->insert($podcast, true); - - $user_podcast_model = new \App\Models\UserPodcastModel(); - $user_podcast_model->save([ - 'user_id' => user()->id, - 'podcast_id' => $new_podcast_id, - ]); + $db->transStart(); + if (!($new_podcast_id = $podcast_model->insert($podcast, true))) { $db->transComplete(); - - return redirect()->route('podcast_list', [$podcast->name]); + return redirect() + ->back() + ->withInput() + ->with('errors', $podcast_model->errors()); } + + $podcast_model->addContributorToPodcast(user()->id, $new_podcast_id); + + $db->transComplete(); + + return redirect()->route('podcast_list'); } public function edit() { - helper(['form', 'misc']); + helper('form'); - if ( - !$this->validate([ - 'title' => 'required', - 'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]', - 'description' => 'required|max_length[4000]', - 'image' => - 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', - 'owner_email' => 'required|valid_email', - 'type' => 'required', - ]) - ) { - $languageModel = new LanguageModel(); - $categoryModel = new CategoryModel(); - $data = [ - 'podcast' => $this->podcast, - 'languages' => $languageModel->findAll(), - 'categories' => $categoryModel->findAll(), - ]; - - echo view('admin/podcast/edit', $data); - } else { - $this->podcast->title = $this->request->getVar('title'); - $this->podcast->name = $this->request->getVar('name'); - $this->podcast->description = $this->request->getVar('description'); - $this->podcast->episode_description_footer = $this->request->getVar( - 'episode_description_footer' - ); + $languageModel = new LanguageModel(); + $categoryModel = new CategoryModel(); + $data = [ + 'podcast' => $this->podcast, + 'languages' => $languageModel->findAll(), + 'categories' => $categoryModel->findAll(), + ]; - $image = $this->request->getFile('image'); - if ($image->isValid()) { - $this->podcast->image = $this->request->getFile('image'); - } - $this->podcast->language = $this->request->getVar('language'); - $this->podcast->category = $this->request->getVar('category'); - $this->podcast->explicit = - ($this->request->getVar('explicit') or false); - $this->podcast->author_name = $this->request->getVar('author_name'); - $this->podcast->author_email = $this->request->getVar( - 'author_email' - ); - $this->podcast->owner_name = $this->request->getVar('owner_name'); - $this->podcast->owner_email = $this->request->getVar('owner_email'); - $this->podcast->type = $this->request->getVar('type'); - $this->podcast->copyright = $this->request->getVar('copyright'); - $this->podcast->block = ($this->request->getVar('block') or false); - $this->podcast->complete = - ($this->request->getVar('complete') or false); - $this->podcast->custom_html_head = $this->request->getVar( - 'custom_html_head' - ); + echo view('admin/podcast/edit', $data); + } - $podcast_model = new PodcastModel(); - $podcast_model->save($this->podcast); + public function attemptEdit() + { + $rules = [ + 'image' => + 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + $this->podcast->title = $this->request->getPost('title'); + $this->podcast->name = $this->request->getPost('name'); + $this->podcast->description = $this->request->getPost('description'); + $this->podcast->episode_description_footer = $this->request->getPost( + 'episode_description_footer' + ); + + $image = $this->request->getFile('image'); + if ($image->isValid()) { + $this->podcast->image = $image; + } + $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->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->custom_html_head = $this->request->getPost( + 'custom_html_head' + ); - return redirect()->route('podcast_list', [$this->podcast->name]); + $podcast_model = new PodcastModel(); + + if (!$podcast_model->save($this->podcast)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $podcast_model->errors()); } + + return redirect()->route('podcast_list'); } public function delete() diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php index 4faffc236339bf10dff8cab77c9eb1deadb6b671..4a8342f50a37ee220d3c9f612b3fc50005a35d7e 100644 --- a/app/Controllers/Admin/User.php +++ b/app/Controllers/Admin/User.php @@ -17,12 +17,9 @@ class User extends BaseController { if (count($params) > 0) { $user_model = new UserModel(); - if ( - !($user = $user_model->where('username', $params[0])->first()) - ) { + if (!($this->user = $user_model->find($params[0]))) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - $this->user = $user; } return $this->$method(); @@ -38,6 +35,11 @@ class User extends BaseController } public function create() + { + echo view('admin/user/create'); + } + + public function attemptCreate() { $user_model = new UserModel(); @@ -53,30 +55,33 @@ class User extends BaseController ); if (!$this->validate($rules)) { - echo view('admin/user/create'); - } else { - // Save the user - $user = new \Myth\Auth\Entities\User($this->request->getPost()); - - // Activate user - $user->activate(); - - // Force user to reset his password on first connection - $user->force_pass_reset = true; - $user->generateResetHash(); - - if (!$user_model->save($user)) { - return redirect() - ->back() - ->withInput() - ->with('errors', $user_model->errors()); - } + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + // Save the user + $user = new \Myth\Auth\Entities\User($this->request->getPost()); - // Success! + // Activate user + $user->activate(); + + // Force user to reset his password on first connection + $user->force_pass_reset = true; + $user->generateResetHash(); + + if (!$user_model->save($user)) { return redirect() - ->route('user_list') - ->with('message', lang('User.createSuccess')); + ->back() + ->withInput() + ->with('errors', $user_model->errors()); } + + // Success! + return redirect() + ->route('user_list') + ->with('message', lang('User.createSuccess')); } public function forcePassReset() diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index 4721a6582c5ebd2c1ef39e29184893bb74d7ad9e..b5174ad637cad6906d8e6b348072cd583b0ebe63 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -24,7 +24,7 @@ class Episode extends BaseController if (count($params) > 1) { $episode_model = new EpisodeModel(); if ( - !($episode = $episode_model + !($this->episode = $episode_model ->where([ 'podcast_id' => $this->podcast->id, 'slug' => $params[1], @@ -33,7 +33,6 @@ class Episode extends BaseController ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - $this->episode = $episode; } return $this->$method(); diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 70db9d60594f0dc1e59690d08a9933e2206ada90..5fd0e14123361ee3459a1b6e88e154a77cd7bafb 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -17,11 +17,12 @@ class Podcast extends BaseController if (count($params) > 0) { $podcast_model = new PodcastModel(); if ( - !($podcast = $podcast_model->where('name', $params[0])->first()) + !($this->podcast = $podcast_model + ->where('name', $params[0]) + ->first()) ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - $this->podcast = $podcast; } return $this->$method(); 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 4cf12a612bd9d241e4d77f45c21e2d0a4b05446f..cd35fe3797a4040496684ee005bf7d8555652b99 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -81,17 +81,23 @@ class AddPodcasts extends Migration '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, - 'owner_name' => + '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, - 'owner_email' => + '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, ], @@ -147,6 +153,7 @@ class AddPodcasts extends Migration ], ]); $this->forge->addKey('id', true); + $this->forge->addForeignKey('owner_id', 'users', 'id'); $this->forge->createTable('podcasts'); } diff --git a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php index 16381819dce947c06352d355de6eb0073c09ea97..abed46d5166f9a33ddd356aa48297e487bb3716d 100644 --- a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php +++ b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php @@ -17,12 +17,6 @@ class AddUsersPodcasts extends Migration public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'user_id' => [ 'type' => 'INT', 'constraint' => 11, @@ -34,8 +28,7 @@ class AddUsersPodcasts extends Migration 'unsigned' => true, ], ]); - $this->forge->addPrimaryKey('id'); - $this->forge->addUniqueKey(['user_id', 'podcast_id']); + $this->forge->addPrimaryKey(['user_id', 'podcast_id']); $this->forge->addForeignKey('user_id', 'users', 'id'); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); $this->forge->createTable('users_podcasts'); diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php new file mode 100644 index 0000000000000000000000000000000000000000..cbbbe4f1e54e7a9ecba7140b4e576c602d3e9b13 --- /dev/null +++ b/app/Database/Seeds/AuthSeeder.php @@ -0,0 +1,153 @@ +<?php +/** + * Class PermissionSeeder + * Inserts permissions + * + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Seeds; + +use CodeIgniter\Database\Seeder; + +class AuthSeeder extends Seeder +{ + public function run() + { + helper('auth'); + + $groups = [['id' => 1, 'name' => 'superadmin', 'description' => '']]; + + /** Build permissions array as a list of: + * + * ``` + * context => [ + * [action, description], + * [action, description], + * ... + * ] + * ``` + */ + $permissions = [ + 'users' => [ + ['name' => 'create', 'description' => 'Create a user'], + ['name' => 'list', 'description' => 'List all users'], + [ + 'name' => 'manage_authorizations', + 'description' => + 'Add or remove roles/permissions to a user', + ], + [ + 'name' => 'manage_bans', + 'description' => 'Ban / unban a user', + ], + [ + 'name' => 'force_pass_reset', + 'description' => + 'Force a user to update his password upon next login', + ], + [ + 'name' => 'delete', + 'description' => + 'Delete user without removing him from database', + ], + [ + 'name' => 'delete_permanently', + 'description' => + 'Delete all occurrences of a user from the database', + ], + ], + 'podcasts' => [ + ['name' => 'create', 'description' => 'Add a new podcast'], + [ + 'name' => 'list', + 'description' => 'List all podcasts and their episodes', + ], + ['name' => 'edit', 'description' => 'Edit any podcast'], + [ + 'name' => 'manage_contributors', + 'description' => 'Add / remove contributors to a podcast', + ], + [ + 'name' => 'manage_publication', + 'description' => 'Publish / unpublish a podcast', + ], + [ + 'name' => 'delete', + 'description' => + 'Delete a podcast without removing it from database', + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete any podcast from the database', + ], + ], + 'episodes' => [ + [ + 'name' => 'list', + 'description' => 'List all episodes of any podcast', + ], + [ + 'name' => 'create', + 'description' => 'Add a new episode to any podcast', + ], + ['name' => 'edit', 'description' => 'Edit any podcast episode'], + [ + 'name' => 'manage_publications', + 'description' => 'Publish / unpublish any podcast episode', + ], + [ + 'name' => 'delete', + 'description' => + 'Delete any podcast episode without removing it from database', + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete any podcast episode from database', + ], + ], + ]; + + // Map permissions to a format the `auth_permissions` table expects + $data_permissions = []; + $data_groups_permissions = []; + $permission_id = 0; + foreach ($permissions as $context => $actions) { + foreach ($actions as $action) { + array_push($data_permissions, [ + 'id' => ++$permission_id, + 'name' => get_permission($context, $action['name']), + 'description' => $action['description'], + ]); + + // add all permissions to superadmin + array_push($data_groups_permissions, [ + 'group_id' => 1, + 'permission_id' => $permission_id, + ]); + } + } + + $this->db->table('auth_permissions')->insertBatch($data_permissions); + $this->db->table('auth_groups')->insertBatch($groups); + $this->db + ->table('auth_groups_permissions') + ->insertBatch($data_groups_permissions); + + // TODO: Remove superadmin user as it is used for testing purposes + $this->db->table('users')->insert([ + 'id' => 1, + 'username' => 'admin', + 'email' => 'admin@castopod.com', + 'password_hash' => + // password: AGUehL3P + '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6', + 'active' => 1, + ]); + $this->db + ->table('auth_groups_users') + ->insert(['group_id' => 1, 'user_id' => 1]); + } +} diff --git a/app/Database/Seeds/UserSeeder.php b/app/Database/Seeds/UserSeeder.php deleted file mode 100644 index 826f1332a17737dfbb62ff7f5ec6db90a46cf6f6..0000000000000000000000000000000000000000 --- a/app/Database/Seeds/UserSeeder.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/** - * Class UserSeeder - * Inserts 'admin' user in users table for testing purposes - * - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Database\Seeds; - -use CodeIgniter\Database\Seeder; - -class UserSeeder extends Seeder -{ - public function run() - { - $data = [ - 'username' => 'admin', - 'email' => 'admin@castopod.com', - 'password_hash' => - // password: AGUehL3P - '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6', - 'active' => 1, - ]; - - $this->db->table('users')->insert($data); - } -} diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 386be65c3f3d84032f74f0cdeaeb16fe227bd05f..edd59e2b10cf89a34b3f2ab91bbebe6c28529450 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -9,6 +9,7 @@ namespace App\Entities; use App\Models\EpisodeModel; use CodeIgniter\Entity; +use Myth\Auth\Models\UserModel; class Podcast extends Entity { @@ -17,6 +18,8 @@ class Podcast extends Entity protected $image_media_path; protected $image_url; protected $episodes; + protected $owner; + protected $contributors; protected $casts = [ 'id' => 'integer', @@ -29,6 +32,7 @@ class Podcast extends Entity 'explicit' => 'boolean', 'author_name' => '?string', 'author_email' => '?string', + 'owner_id' => 'integer', 'owner_name' => '?string', 'owner_email' => '?string', 'type' => 'string', @@ -92,7 +96,7 @@ class Podcast extends Entity ); } - if (empty($this->permissions)) { + if (empty($this->episodes)) { $this->episodes = (new EpisodeModel())->getPodcastEpisodes( $this->id ); @@ -100,4 +104,40 @@ class Podcast extends Entity return $this->episodes; } + + /** + * Returns the podcast owner + * + * @return \Myth\Auth\Entities\User + */ + public function getOwner() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting owner.' + ); + } + + if (empty($this->owner)) { + $this->owner = (new UserModel())->find($this->owner_id); + } + + return $this->owner; + } + + public function setOwner(\Myth\Auth\Entities\User $user) + { + $this->attributes['owner_id'] = $user->id; + + return $this; + } + + public function getContributors() + { + return (new UserModel()) + ->select('users.*') + ->join('users_podcasts', 'users_podcasts.user_id = users.id') + ->where('users_podcasts.podcast_id', $this->attributes['id']) + ->findAll(); + } } diff --git a/app/Entities/UserPodcast.php b/app/Entities/UserPodcast.php deleted file mode 100644 index d951557dc9e432a3d682dc72e99e61d90c9a51f9..0000000000000000000000000000000000000000 --- a/app/Entities/UserPodcast.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php -/** - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Entities; - -use CodeIgniter\Entity; - -class UserPodcast extends Entity -{ - protected $casts = [ - 'user_id' => 'integer', - 'podcast_id' => 'integer', - ]; -} diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..344e0636a2a8bd632996c8a080ee4670e5c5c1c1 --- /dev/null +++ b/app/Helpers/auth_helper.php @@ -0,0 +1,19 @@ +<?php +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +/** + * Gets the permission name by concatenating the context and action + * + * @param string $context + * @param string $action + * + * @return string permission name + */ +function get_permission($context, $action) +{ + return $context . '-' . $action; +} diff --git a/app/Language/en/Contributor.php b/app/Language/en/Contributor.php new file mode 100644 index 0000000000000000000000000000000000000000..552c9f64fc946619b13bde5979b1c6f3df8f9027 --- /dev/null +++ b/app/Language/en/Contributor.php @@ -0,0 +1,21 @@ +<? +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'podcast_contributors' => 'Podcast contributors', + 'add' => 'Add contributor', + 'add_contributor' => 'Add a contributor for {0}', + 'edit_role' => 'Update role for {0}', + 'edit' => 'Edit', + 'remove' => 'Remove', + 'form' => [ + 'user' => 'User', + 'role' => 'Role', + 'submit_add' => 'Add contributor', + 'submit_edit' => 'Update role' + ] +]; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 22ff6a53495130f73b3bc476e0ffe7da506df3e3..80ae2df6b3971850038d8c27fc9dbd80cd84e68e 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -7,7 +7,6 @@ return [ 'all_podcast_episodes' => 'All podcast episodes', - 'create_one' => 'Add a new one', 'back_to_podcast' => 'Go back to podcast', 'edit' => 'Edit', 'delete' => 'Delete', diff --git a/app/Language/en/MyAccount.php b/app/Language/en/MyAccount.php index c1935bcc3331216129afd04242244f2162a914fd..f32684b7eeaa6a2e5237149d1835e004a4789f61 100644 --- a/app/Language/en/MyAccount.php +++ b/app/Language/en/MyAccount.php @@ -7,5 +7,6 @@ return [ 'passwordChangeSuccess' => 'Password has been successfully changed!', - 'changePassword' => 'Change my password' + 'changePassword' => 'Change my password', + 'info' => 'My account info' ]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 767d54e08970bf74283c1a8b2542c19a86118f9a..54601135025fcaa163387110efeacefb427fbe9a 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -8,13 +8,13 @@ return [ 'all_podcasts' => 'All podcasts', 'no_podcast' => 'No podcast found!', - 'create_one' => 'Add a new one', 'create' => 'Create a Podcast', 'new_episode' => 'New Episode', 'feed' => 'RSS feed', 'edit' => 'Edit', 'delete' => 'Delete', 'see_episodes' => 'See episodes', + 'see_contributors' => 'See contributors', 'goto_page' => 'Go to page', 'form' => [ 'title' => 'Title', diff --git a/app/Language/en/User.php b/app/Language/en/User.php index fcf9134c643a0023c2d5024e153a30ef95f3a4af..c9e0fc03ccd8d15f37375a507c4b53acb1c4f7f4 100644 --- a/app/Language/en/User.php +++ b/app/Language/en/User.php @@ -10,11 +10,13 @@ return [ 'forcePassResetSuccess' => 'The user will be prompted with a password reset during his next login attempt.', 'banSuccess' => 'User has been banned.', 'unbanSuccess' => 'User has been unbanned.', + 'deleteSuccess' => 'User has been deleted.', 'forcePassReset' => 'Force pass reset', 'ban' => 'Ban', 'unban' => 'Unban', 'delete' => 'Delete', 'create' => 'Create a user', + 'all_users' => 'All users', 'form' => [ 'email' => 'Email', 'username' => 'Username', diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index ed2ea4e9f84b5e70c4e8d040c0f4f9e5cd2adcee..eb372fa4018efc6961f77c87aedf562953311009 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -19,11 +19,8 @@ class EpisodeModel extends Model 'title', 'slug', 'enclosure_uri', - 'enclosure_length', - 'enclosure_type', 'pub_date', 'description', - 'duration', 'image_uri', 'explicit', 'number', @@ -39,6 +36,21 @@ class EpisodeModel extends Model protected $useSoftDeletes = true; protected $useTimestamps = true; + protected $validationRules = [ + 'podcast_id' => 'required', + 'title' => 'required', + 'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]', + 'enclosure_uri' => 'required', + 'pub_date' => 'required|valid_date', + 'description' => 'required', + 'image_uri' => 'required', + 'number' => 'required', + 'season_number' => 'required', + 'author_email' => 'valid_email|permit_empty', + 'type' => 'required', + ]; + protected $validationMessages = []; + protected $afterInsert = ['writeEnclosureMetadata', 'clearCache']; protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache']; diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 09712b6a4e63c035c0471feaf84c2d2db9740e31..8d7365a98362030d6b7dbe8cc55c55a23d4114d1 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -8,6 +8,8 @@ namespace App\Models; use CodeIgniter\Model; +use Myth\Auth\Authorization\GroupModel; +use Myth\Auth\Config\Services; class PodcastModel extends Model { @@ -26,6 +28,7 @@ class PodcastModel extends Model 'explicit', 'author_name', 'author_email', + 'owner_id', 'owner_name', 'owner_email', 'type', @@ -40,8 +43,60 @@ class PodcastModel extends Model protected $useTimestamps = true; - protected $afterInsert = ['clearCache']; + protected $validationRules = [ + 'title' => 'required', + 'name' => + 'required|regex_match[/^[a-zA-Z0-9\_]{1,191}$/]|is_unique[podcasts.name,id,{id}]', + 'description' => 'required', + 'image_uri' => 'required', + 'language' => 'required', + 'category' => 'required', + 'author_email' => 'valid_email|permit_empty', + 'owner_id' => 'required', + 'owner_email' => 'required|valid_email', + 'type' => 'required', + ]; + protected $validationMessages = []; + + protected $afterInsert = ['clearCache', 'createPodcastPermissions']; protected $afterUpdate = ['clearCache']; + protected $beforeDelete = ['clearCache']; + + /** + * Gets all the podcasts a given user is contributing to + * + * @param int $user_id + * + * @return \App\Entities\Podcast[] podcasts + */ + public function getUserPodcasts($user_id) + { + return $this->select('podcasts.*') + ->join('users_podcasts', 'users_podcasts.podcast_id = podcasts.id') + ->where('users_podcasts.user_id', $user_id) + ->findAll(); + } + + public function addContributorToPodcast($user_id, $podcast_id) + { + $data = [ + 'user_id' => (int) $user_id, + 'podcast_id' => (int) $podcast_id, + ]; + + return $this->db->table('users_podcasts')->insert($data); + } + + public function removeContributorFromPodcast($user_id, $podcast_id) + { + return $this->db + ->table('users_podcasts') + ->where([ + 'user_id' => $user_id, + 'podcast_id' => $podcast_id, + ]) + ->delete(); + } protected function clearCache(array $data) { @@ -56,5 +111,95 @@ class PodcastModel extends Model // foreach ($podcast->episodes as $episode) { // $cache->delete(md5($episode->link)); // } + + $data['podcast'] = $podcast; + + return $data; + } + + protected function createPodcastPermissions(array $data) + { + $authorize = Services::authorization(); + + $podcast = $data['podcast']; + + $podcast_permissions = [ + 'podcasts:' . $podcast->id => [ + [ + 'name' => 'edit', + 'description' => "Edit the $podcast->name podcast", + ], + [ + 'name' => 'delete', + 'description' => "Delete the $podcast->name podcast without removing it from the database", + ], + [ + 'name' => 'delete_permanently', + 'description' => "Delete the $podcast->name podcast from the database", + ], + [ + 'name' => 'manage_contributors', + 'description' => "Add / remove contributors to the $podcast->name podcast and edit their roles", + ], + [ + 'name' => 'manage_publication', + 'description' => "Publish / unpublish $podcast->name", + ], + ], + 'podcasts:' . $podcast->id . ':episodes' => [ + [ + 'name' => 'list', + 'description' => "List all episodes of the $podcast->name podcast", + ], + [ + 'name' => 'create', + 'description' => "Add new episodes for the $podcast->name podcast", + ], + [ + 'name' => 'edit', + 'description' => "Edit an episode of the $podcast->name podcast", + ], + [ + 'name' => 'delete', + 'description' => "Delete an episode of the $podcast->name podcast without removing it from the database", + ], + [ + 'name' => 'delete_permanently', + 'description' => "Delete all occurrences of an episode of the $podcast->name podcast from the database", + ], + [ + 'name' => 'manage_publications', + 'description' => "Publish / unpublish episodes of the $podcast->name podcast", + ], + ], + ]; + + $group_model = new GroupModel(); + $owner_group_id = $group_model->insert( + [ + 'name' => "podcasts:$podcast->id" . '_owner', + 'description' => "The owner of the $podcast->name podcast", + ], + true + ); + + // add podcast owner to owner group + $authorize->addUserToGroup($podcast->owner_id, $owner_group_id); + + foreach ($podcast_permissions as $context => $actions) { + foreach ($actions as $action) { + $permission_id = $authorize->createPermission( + get_permission($context, $action['name']), + $action['description'] + ); + + $authorize->addPermissionToGroup( + $permission_id, + $owner_group_id + ); + } + } + + return $data; } } diff --git a/app/Models/UserPodcastModel.php b/app/Models/UserPodcastModel.php deleted file mode 100644 index 32d36b051269f913ae38f1c928fe64031ddccbd4..0000000000000000000000000000000000000000 --- a/app/Models/UserPodcastModel.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class UserPodcastModel extends Model -{ - protected $table = 'users_podcasts'; - protected $primaryKey = 'id'; - - protected $allowedFields = ['user_id', 'podcast_id']; - - protected $returnType = 'App\Entities\UserPodcast'; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Views/_message_block.php b/app/Views/_message_block.php index 466b5780b94e4651ae0bd30f95c79bf3b13740e8..2504c21135b155c11917ede4f2a5e7abbadf17aa 100644 --- a/app/Views/_message_block.php +++ b/app/Views/_message_block.php @@ -1,17 +1,17 @@ <?php if (session()->has('message')): ?> - <div class="px-4 py-2 font-semibold text-green-900 bg-green-200 border border-green-700"> + <div class="px-4 py-2 mb-4 font-semibold text-green-900 bg-green-200 border border-green-700"> <?= session('message') ?> </div> <?php endif; ?> <?php if (session()->has('error')): ?> - <div class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700"> + <div class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700"> <?= session('error') ?> </div> <?php endif; ?> <?php if (session()->has('errors')): ?> - <ul class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700"> + <ul class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700"> <?php foreach (session('errors') as $error): ?> <li><?= $error ?></li> <?php endforeach; ?> diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index a96b0a7cf4daea8a3d651546dceca4559842f344..711a7ae702b1c9243a301571b32526949e8e2eb6 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> - <title>Castopod</title> + <title>Castopod Admin</title> <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> @@ -12,9 +12,12 @@ <body class="flex flex-col min-h-screen mx-auto"> <header class="text-white bg-gray-900 border-b"> - <div class="flex items-center justify-between px-4 py-4 mx-auto"> - <a href="<?= route_to('home') ?>" class="text-xl">Castopod Admin</a> - <nav> + <div class="flex items-center px-4 py-4 mx-auto"> + <a href="<?= route_to('admin') ?>" class="text-xl">Castopod Admin</a> + <a href="<?= route_to( + 'home' + ) ?>" class="ml-4 text-sm underline hover:no-underline">Go to website</a> + <nav class="ml-auto"> <span class="mr-2">Welcome, <?= user()->username ?></span> <a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to( 'logout' @@ -25,9 +28,8 @@ <div class="flex flex-1"> <?= view('admin/_sidenav') ?> <main class="container flex-1 px-4 py-6 mx-auto"> - <div class="mb-4"> - <?= view('_message_block') ?> - </div> + <h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1> + <?= view('_message_block') ?> <?= $this->renderSection('content') ?> </main> </div> diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php index f68ae5bab786e2dead2f4471e5f1da31034984ed..da7274334f2b591baf0d3b649f426605951fdf93 100644 --- a/app/Views/admin/_sidenav.php +++ b/app/Views/admin/_sidenav.php @@ -8,6 +8,11 @@ <div class="mb-4"> <span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Podcasts</span> <ul> + <li> + <a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to( + 'my_podcasts' + ) ?>">My podcasts</a> + </li> <li> <a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to( 'podcast_list' diff --git a/app/Views/admin/contributor/add.php b/app/Views/admin/contributor/add.php new file mode 100644 index 0000000000000000000000000000000000000000..69bbe71a3957386f43d8df5f3755d739d854ac73 --- /dev/null +++ b/app/Views/admin/contributor/add.php @@ -0,0 +1,48 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= lang('Contributor.add_contributor', [$podcast->title]) ?> +<?= $this->endSection() ?> + + +<?= $this->section('content') ?> +<form action="<?= route_to( + 'contributor_add', + $podcast->id +) ?>" method="post" class="flex flex-col max-w-lg"> + <?= csrf_field() ?> + + <div class="flex flex-col mb-4"> + <label for="user"><?= lang('Contributor.form.user') ?></label> + <select id="user" name="user" autocomplete="off" class="form-select" required> + <?php foreach ($users as $user): ?> + <option value="<?= $user->id ?>" + <?php if ( + old('user') == $user->id + ): ?> selected <?php endif; ?>> + <?= $user->username ?> + </option> + <?php endforeach; ?> + </select> + </div> + + <div class="flex flex-col mb-4"> + <label for="role"><?= lang('Contributor.form.role') ?></label> + <select id="role" name="role" autocomplete="off" class="form-select" required> + <?php foreach ($roles as $role): ?> + <option value="<?= $role->id ?>" + <?php if ( + old('role') == $role->id + ): ?> selected <?php endif; ?>> + <?= $role->name ?> + </option> + <?php endforeach; ?> + </select> + </div> + + <button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang( + 'Contributor.form.submit_add' + ) ?></button> +</form> + +<?= $this->endSection() ?> diff --git a/app/Views/admin/contributor/edit.php b/app/Views/admin/contributor/edit.php new file mode 100644 index 0000000000000000000000000000000000000000..fb80c1a5fa8c94480bde03cb14174a1de2b0fb94 --- /dev/null +++ b/app/Views/admin/contributor/edit.php @@ -0,0 +1,35 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= lang('Contributor.edit_role', [$user->username]) ?> +<?= $this->endSection() ?> + + +<?= $this->section('content') ?> +<form action="<?= route_to( + 'contributor_edit', + $podcast->id, + $user->id +) ?>" method="post" class="flex flex-col max-w-lg"> + <?= csrf_field() ?> + + <div class="flex flex-col mb-4"> + <label for="category"><?= lang('Contributor.form.role') ?></label> + <select id="role" name="role" autocomplete="off" class="form-select" required> + <?php foreach ($roles as $role): ?> + <option value="<?= $role->id ?>" + <?php if ( + old('role') == $role->id + ): ?> selected <?php endif; ?>> + <?= $role->name ?> + </option> + <?php endforeach; ?> + </select> + </div> + + <button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang( + 'Contributor.form.submit_edit' + ) ?></button> + +</form> +<?= $this->endSection() ?> diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php new file mode 100644 index 0000000000000000000000000000000000000000..e018cb7de8a41a65de86b9784abd05148fb6b9ec --- /dev/null +++ b/app/Views/admin/contributor/list.php @@ -0,0 +1,47 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= lang('Contributor.podcast_contributors') ?> +<?= $this->endSection() ?> + + +<?= $this->section('content') ?> +<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to( + 'contributor_add', + $podcast->id +) ?>"><?= lang('Contributor.add') ?></a> + +<table class="table-auto"> + <thead> + <tr> + <th class="px-4 py-2">Username</th> + <th class="px-4 py-2">Permissions</th> + <th class="px-4 py-2">Actions</th> + </tr> + </thead> + <tbody> + <?php foreach ($podcast->contributors as $contributor): ?> + <tr> + <td class="px-4 py-2 border"><?= $contributor->username ?></td> + <td class="px-4 py-2 border">[<?= implode( + ', ', + $contributor->permissions + ) ?>]</td> + <td class="px-4 py-2 border"> + <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( + 'contributor_edit', + $podcast->id, + $contributor->id + ) ?>"><?= lang('Contributor.edit') ?></a> + <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( + 'contributor_remove', + $podcast->id, + $contributor->id + ) ?>"><?= lang('Contributor.remove') ?></a> + </td> + </tr> + <?php endforeach; ?> + </tbody> +</table> + +<?= $this->endSection() ?> diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php index 910003fae670ce1928fa03736eda874597882595..9bbda5012474677097a25915181de48235ddfc97 100644 --- a/app/Views/admin/dashboard.php +++ b/app/Views/admin/dashboard.php @@ -1,8 +1,6 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> - -<h1 class="text-2xl">Welcome to the admin dashboard!</h1> - -<?= $this->endSection() ?> +<?= $this->section( + 'title' +) ?>Welcome to the admin dashboard!<?= $this->endSection() ?> diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php index af1e9d4069a4c5797c6d81d4447c3544385aaf87..b4cd9f58f3eef6b6e11eafab739b7fb14387ea79 100644 --- a/app/Views/admin/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -1,14 +1,13 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('Episode.create') ?> +<?= $this->endSection() ?> -<h1 class="mb-6 text-xl"><?= lang('Episode.create') ?></h1> -<div class="mb-8"> - <?= \Config\Services::validation()->listErrors() ?> -</div> +<?= $this->section('content') ?> -<?= form_open_multipart(route_to('episode_create', $podcast->name), [ +<?= form_open_multipart(route_to('episode_create', $podcast->id), [ 'method' => 'post', 'class' => 'flex flex-col max-w-md', ]) ?> @@ -21,24 +20,30 @@ <div class="flex flex-col mb-4"> <label for="title"><?= lang('Episode.form.title') ?></label> - <input type="text" class="form-input" id="title" name="title" required /> + <input type="text" class="form-input" id="title" name="title" required value="<?= old( + 'title' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="slug"><?= lang('Episode.form.slug') ?></label> - <input type="text" class="form-input" id="slug" name="slug" required /> + <input type="text" class="form-input" id="slug" name="slug" required value="<?= old( + 'slug' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="description"><?= lang('Episode.form.description') ?></label> - <textarea class="form-textarea" id="description" name="description" required></textarea> + <textarea class="form-textarea" id="description" name="description" required><?= old( + 'description' + ) ?></textarea> </div> <div class="flex flex-col mb-4"> <label for="pub_date"><?= lang('Episode.form.pub_date') ?></label> - <input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date( - 'Y-m-d' - ) ?>" /> + <input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= old( + 'pub_date' + ) || date('Y-m-d') ?>" /> </div> <div class="flex flex-col mb-4"> @@ -50,16 +55,22 @@ <label for="episode_number"><?= lang( 'Episode.form.episode_number' ) ?></label> - <input type="number" class="form-input" id="episode_number" name="episode_number" required /> + <input type="number" class="form-input" id="episode_number" name="episode_number" required value="<?= old( + 'episode_number' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="season_number"><?= lang('Episode.form.season_number') ?></label> - <input type="number" class="form-input" id="season_number" name="season_number" /> + <input type="number" class="form-input" id="season_number" name="season_number" value="<?= old( + 'season_number' + ) ?>" /> </div> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" /> + <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if ( + old('explicit') + ): ?> checked <?php endif; ?> /> <label for="explicit" class="pl-2"><?= lang( 'Episode.form.explicit' ) ?></label> @@ -67,32 +78,45 @@ <div class="flex flex-col mb-4"> <label for="author_name"><?= lang('Podcast.form.author_name') ?></label> - <input type="text" class="form-input" id="author_name" name="author_name" /> + <input type="text" class="form-input" id="author_name" name="author_name" value="<?= old( + 'author_name' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="author_email"><?= lang('Podcast.form.author_email') ?></label> - <input type="email" class="form-input" id="author_email" name="author_email" /> + <input type="email" class="form-input" id="author_email" name="author_email" value="<?= old( + 'author_email' + ) ?>" /> </div> <fieldset class="flex flex-col mb-4"> <legend><?= lang('Episode.form.type.label') ?></legend> <label for="full" class="inline-flex items-center"> - <input type="radio" class="form-radio" value="full" id="full" name="type" required checked /> + <input type="radio" class="form-radio" value="full" id="full" name="type" required <?php if ( + !old('type') || + old('type') == 'full' + ): ?> checked <?php endif; ?> /> <span class="ml-2"><?= lang('Episode.form.type.full') ?></span> </label> <label for="trailer" class="inline-flex items-center"> - <input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required /> + <input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required <?php if ( + old('type') == 'trailer' + ): ?> checked <?php endif; ?> /> <span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span> </label> <label for="bonus" class="inline-flex items-center"> - <input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required /> + <input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required <?php if ( + old('type') == 'bonus' + ): ?> checked <?php endif; ?> /> <span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span> </label> </fieldset> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="block" name="block" class="form-checkbox" /> + <input type="checkbox" id="block" name="block" class="form-checkbox" <?php if ( + old('block') + ): ?> checked <?php endif; ?> /> <label for="block" class="pl-2"><?= lang('Episode.form.block') ?></label> </div> diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php index 51b91d984d74ccd16822daf1c8da8f1ad29ab37e..3692d768517116760c98cdd11054388ce13ca5a3 100644 --- a/app/Views/admin/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -1,20 +1,16 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('Episode.edit') ?> +<?= $this->endSection() ?> -<h1 class="mb-6 text-xl"><?= lang('Episode.edit') ?></h1> -<div class="mb-8"> - <?= \Config\Services::validation()->listErrors() ?> -</div> +<?= $this->section('content') ?> -<?= form_open_multipart( - route_to('episode_edit', $podcast->name, $episode->slug), - [ - 'method' => 'post', - 'class' => 'flex flex-col max-w-md', - ] -) ?> +<?= form_open_multipart(route_to('episode_edit', $podcast->id, $episode->id), [ + 'method' => 'post', + 'class' => 'flex flex-col max-w-md', +]) ?> <?= csrf_field() ?> <div class="flex flex-col mb-4"> diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index 81d4c1f9a373ac49554c5aa0453a5e23b6a87381..54862ad4891c4d24bf3f5e9716dbca91f5c17856 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -1,21 +1,29 @@ <?= $this->extend('admin/_layout') ?> +<?= $this->section('title') ?> + +<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>) + +<?= $this->endSection() ?> + + <?= $this->section('content') ?> +<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to( + 'episode_create', + $podcast->id +) ?>"><?= lang('Episode.create') ?></a> <div class="flex flex-col py-4"> - <h1 class="mb-4 text-xl"><?= lang( - 'Episode.all_podcast_episodes' - ) ?> (<?= count($all_podcast_episodes) ?>)</h1> - <?php if ($all_podcast_episodes): ?> - <?php foreach ($all_podcast_episodes as $episode): ?> + <?php if ($podcast->episodes): ?> + <?php foreach ($podcast->episodes as $episode): ?> <article class="flex-col w-full max-w-lg p-4 mb-4 border shadow"> <div class="flex mb-2"> <img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" /> <div class="flex flex-col flex-1"> <a href="<?= route_to( 'episode_edit', - $podcast->name, - $episode->slug + $podcast->id, + $episode->id ) ?>"> <h3 class="text-xl font-semibold"> <span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span> @@ -24,15 +32,15 @@ <p><?= $episode->description ?></p> </a> <audio controls class="mt-auto" preload="none"> - <source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>"> + <source src="<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>"> Your browser does not support the audio tag. </audio> </div> </div> <a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( 'episode_edit', - $podcast->name, - $episode->slug + $podcast->id, + $episode->id ) ?>"><?= lang('Episode.edit') ?></a> <a href="<?= route_to( 'episode', @@ -43,21 +51,15 @@ ) ?></a> <a href="<?= route_to( 'episode_delete', - $podcast->name, - $episode->slug + $podcast->id, + $episode->id ) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang( 'Episode.delete' ) ?></a> </article> <?php endforeach; ?> <?php else: ?> - <div class="flex items-center"> - <p class="mr-4 italic"><?= lang('Podcast.no_episode') ?></p> - <a class="self-start px-4 py-2 border hover:bg-gray-100" href="<?= route_to( - 'episode_create', - $podcast->name - ) ?>"><?= lang('Episode.create_one') ?></a> - </div> + <p class="italic"><?= lang('Podcast.no_episode') ?></p> <?php endif; ?> </div> diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php index 28cc35eaaa62c0e651837f3eb3910088c1b17925..619085d8261b35e71083cfe96a13389a21b8fef3 100644 --- a/app/Views/admin/my_account/change_password.php +++ b/app/Views/admin/my_account/change_password.php @@ -1,8 +1,11 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('MyAccount.changePassword') ?> +<?= $this->endSection() ?> + -<h1 class="mb-6 text-xl"><?= lang('MyAccount.changePassword') ?></h1> +<?= $this->section('content') ?> <form action="<?= route_to( 'myAccount_changePassword' diff --git a/app/Views/admin/my_account/view.php b/app/Views/admin/my_account/view.php index 03deb336a2084987ce88d69eb0a41123567f6248..92ce2ed5b330879f9a9c67e1b68ba56b76970bdf 100644 --- a/app/Views/admin/my_account/view.php +++ b/app/Views/admin/my_account/view.php @@ -1,5 +1,10 @@ <?= $this->extend('admin/_layout') ?> +<?= $this->section('title') ?> +<?= lang('MyAccount.info') ?> +<?= $this->endSection() ?> + + <?= $this->section('content') ?> <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index c9eed74eb1b8e47b54151ceeb22a3344230ce216..399fd2900c9aa38495711ff676640d10396b64c3 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -1,14 +1,13 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('Podcast.create') ?> +<?= $this->endSection() ?> -<h1 class="mb-6 text-xl"><?= lang('Podcast.create') ?></h1> -<div class="mb-8"> - <?= \Config\Services::validation()->listErrors() ?> -</div> +<?= $this->section('content') ?> -<?= form_open_multipart(base_url(route_to('podcast_create')), [ +<?= form_open_multipart(route_to('podcast_create'), [ 'method' => 'post', 'class' => 'flex flex-col max-w-md', ]) ?> @@ -16,24 +15,32 @@ <div class="flex flex-col mb-4"> <label for="title"><?= lang('Podcast.form.title') ?></label> - <input type="text" class="form-input" id="title" name="title" required /> + <input type="text" class="form-input" id="title" name="title" value="<?= old( + 'title' + ) ?>" required /> </div> <div class="flex flex-col mb-4"> <label for="name"><?= lang('Podcast.form.name') ?></label> - <input type="text" class="form-input" id="name" name="name" required /> + <input type="text" class="form-input" id="name" name="name" value="<?= old( + 'name' + ) ?>" required /> </div> <div class="flex flex-col mb-4"> <label for="description"><?= lang('Podcast.form.description') ?></label> - <textarea class="form-textarea" id="description" name="description" required></textarea> + <textarea class="form-textarea" id="description" name="description" required><?= old( + 'description' + ) ?></textarea> </div> <div class="flex flex-col mb-4"> <label for="episode_description_footer"><?= lang( 'Podcast.form.episode_description_footer' ) ?></label> - <textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"></textarea> + <textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"><?= old( + 'episode_description_footer' + ) ?></textarea> </div> <div class="flex flex-col mb-4"> @@ -45,9 +52,15 @@ <label for="language"><?= lang('Podcast.form.language') ?></label> <select id="language" name="language" autocomplete="off" class="form-select" required> <?php foreach ($languages as $language): ?> - <option <?= $language->code == $browser_lang - ? "selected='selected'" - : '' ?> value="<?= $language->code ?>"> + <option value="<?= $language->code ?>" + <?php if ( + old('language') == $language->code + ): ?> selected <?php endif; ?> + <?php if ( + !old('language') && + $language->code == $browser_lang + ): ?> selected <?php endif; ?> + > <?= $language->native_name ?> </option> <?php endforeach; ?> @@ -58,16 +71,20 @@ <label for="category"><?= lang('Podcast.form.category') ?></label> <select id="category" name="category" class="form-select" required> <?php foreach ($categories as $category): ?> - <option value="<?= $category->code ?>"><?= lang( - 'Podcast.category_options.' . $category->code -) ?> + <option value="<?= $category->code ?>" + <?php if ( + old('category') == $category->code + ): ?> selected <?php endif; ?> + ><?= lang('Podcast.category_options.' . $category->code) ?> </option> <?php endforeach; ?> </select> </div> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" /> + <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if ( + old('explicit') + ): ?> checked <?php endif; ?> /> <label for="explicit" class="pl-2"><?= lang( 'Podcast.form.explicit' ) ?></label> @@ -75,48 +92,67 @@ <div class="flex flex-col mb-4"> <label for="author_name"><?= lang('Podcast.form.author_name') ?></label> - <input type="text" class="form-input" id="author_name" name="author_name" /> + <input type="text" class="form-input" id="author_name" name="author_name" value="<?= old( + 'author_name' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="author_email"><?= lang('Podcast.form.author_email') ?></label> - <input type="email" class="form-input" id="author_email" name="author_email" /> + <input type="email" class="form-input" id="author_email" name="author_email" value="<?= old( + 'author_email' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="owner_name"><?= lang('Podcast.form.owner_name') ?></label> - <input type="text" class="form-input" id="owner_name" name="owner_name" /> + <input type="text" class="form-input" id="owner_name" name="owner_name" value="<?= old( + 'owner_name' + ) ?>" /> </div> <div class="flex flex-col mb-4"> <label for="owner_email"><?= lang('Podcast.form.owner_email') ?></label> - <input type="email" class="form-input" id="owner_email" name="owner_email" required /> + <input type="email" class="form-input" id="owner_email" name="owner_email" value="<?= old( + 'owner_email' + ) ?>" required /> </div> <fieldset class="flex flex-col mb-4"> <legend><?= lang('Podcast.form.type.label') ?></legend> <label for="episodic" class="inline-flex items-center"> - <input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required checked /> - <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span> + <input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required <?php if ( + !old('type') || + old('type') == 'episodic' + ): ?> checked <?php endif; ?> /> + <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span> </label> <label for="serial" class="inline-flex items-center"> - <input type="radio" class="form-radio" value="serial" id="serial" name="type" required /> - <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span> + <input type="radio" class="form-radio" value="serial" id="serial" name="type" required <?php if ( + old('type') == 'serial' + ): ?> checked <?php endif; ?> /> + <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span> </label> </fieldset> <div class="flex flex-col mb-4"> <label for="copyright"><?= lang('Podcast.form.copyright') ?></label> - <input type="text" class="form-input" id="copyright" name="copyright" /> + <input type="text" class="form-input" id="copyright" name="copyright" value="<?= old( + 'copyright' + ) ?>" /> </div> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="block" name="block" class="form-checkbox" /> + <input type="checkbox" id="block" name="block" class="form-checkbox" <?php if ( + old('block') + ): ?> checked <?php endif; ?> /> <label for="block" class="pl-2"><?= lang('Podcast.form.block') ?></label> </div> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="complete" name="complete" class="form-checkbox" /> + <input type="checkbox" id="complete" name="complete" class="form-checkbox" <?php if ( + old('complete') + ): ?> checked <?php endif; ?> /> <label for="complete" class="pl-2"><?= lang( 'Podcast.form.complete' ) ?></label> diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php index 4dcfff666b85ca716f4d0902722e092e93216e02..653d2482e7c2b0847ac55c113b9fde53b785747a 100644 --- a/app/Views/admin/podcast/edit.php +++ b/app/Views/admin/podcast/edit.php @@ -1,14 +1,13 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('Podcast.edit') ?> +<?= $this->endSection() ?> -<h1 class="mb-6 text-xl"><?= lang('Podcast.edit') ?></h1> -<div class="mb-8"> - <?= \Config\Services::validation()->listErrors() ?> -</div> +<?= $this->section('content') ?> -<?= form_open_multipart(base_url(route_to('podcast_edit', $podcast->name)), [ +<?= form_open_multipart(route_to('podcast_edit', $podcast->id), [ 'method' => 'post', 'class' => 'flex flex-col max-w-md', ]) ?> @@ -70,7 +69,9 @@ </div> <div class="inline-flex items-center mb-4"> - <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" checked="<?= $podcast->explicit ?>" /> + <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?= $podcast->explicit + ? 'checked' + : '' ?> /> <label for="explicit" class="pl-2"><?= lang( 'Podcast.form.explicit' ) ?></label> @@ -123,7 +124,7 @@ <div class="inline-flex items-center mb-4"> <input type="checkbox" id="complete" name="complete" class="form-checkbox" - <?= $podcast->block ? 'checked' : '' ?> /> + <?= $podcast->complete ? 'checked' : '' ?> /> <label for="complete" class="pl-2"><?= lang( 'Podcast.form.complete' ) ?></label> diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php index 7510eeef26b29c37cb52cf50932e3f512fb29198..c3fd22cd91094b151ef3be99722b86c9ad6bacb7 100644 --- a/app/Views/admin/podcast/list.php +++ b/app/Views/admin/podcast/list.php @@ -1,10 +1,15 @@ <?= $this->extend('admin/_layout') ?> +<?= $this->section('title') ?> +<?= lang('Podcast.all_podcasts') ?> (<?= count($all_podcasts) ?>) +<?= $this->endSection() ?> + + <?= $this->section('content') ?> -<h1 class="mb-2 text-xl"><?= lang('Podcast.all_podcasts') ?> (<?= count( - $all_podcasts - ) ?>)</h1> +<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to( + 'podcast_create' +) ?>"><?= lang('Podcast.create') ?></a> <div class="flex flex-wrap"> <?php if ($all_podcasts): ?> <?php foreach ($all_podcasts as $podcast): ?> @@ -12,36 +17,36 @@ <img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" /> <a href="<?= route_to( 'episode_list', - $podcast->name + $podcast->id ) ?>" class="hover:underline"> <h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2> </a> <p class="mb-4 text-gray-600">@<?= $podcast->name ?></p> <a class="inline-flex px-2 py-1 mb-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( 'podcast_edit', - $podcast->name + $podcast->id ) ?>"><?= lang('Podcast.edit') ?></a> <a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to( 'episode_list', - $podcast->name + $podcast->id ) ?>"><?= lang('Podcast.see_episodes') ?></a> + <a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to( + 'contributor_list', + $podcast->id + ) ?>"><?= lang('Podcast.see_contributors') ?></a> <a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to( 'podcast', $podcast->name ) ?>"><?= lang('Podcast.goto_page') ?></a> <a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to( 'podcast_delete', - $podcast->name + $podcast->id ) ?>"><?= lang('Podcast.delete') ?></a> + </article> <?php endforeach; ?> <?php else: ?> - <div class="flex items-center"> - <p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p> - <a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to( - 'podcast_create' - ) ?>"><?= lang('Podcast.create_one') ?></a> - </div> + <p class="italic"><?= lang('Podcast.no_podcast') ?></p> <?php endif; ?> </div> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php index 82bfe99d2547137b33a8770f5cff1bfdf87d5b2d..865521ce52edfcaae6e9c4bbf7f6d7f4b6320e02 100644 --- a/app/Views/admin/user/create.php +++ b/app/Views/admin/user/create.php @@ -1,12 +1,11 @@ <?= $this->extend('admin/_layout') ?> -<?= $this->section('content') ?> +<?= $this->section('title') ?> +<?= lang('User.create') ?> +<?= $this->endSection() ?> -<h1 class="mb-6 text-xl"><?= lang('User.create') ?></h1> -<div class="mb-8"> - <?= \Config\Services::validation()->listErrors() ?> -</div> +<?= $this->section('content') ?> <form action="<?= route_to( 'user_create' diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php index a0d121213b8e6b697fe1f274abe57c77a07512ac..b41502250d700da89d6408f6f6e0e4702b9027e2 100644 --- a/app/Views/admin/user/list.php +++ b/app/Views/admin/user/list.php @@ -1,61 +1,55 @@ <?= $this->extend('admin/_layout') ?> +<?= $this->section('title') ?> +<?= lang('User.all_users') ?> (<?= count($all_users) ?>) +<?= $this->endSection() ?> + + <?= $this->section('content') ?> -<div class="flex flex-wrap"> - <?php if ($all_users): ?> - <table class="table-auto"> - <thead> - <tr> - <th class="px-4 py-2">Username</th> - <th class="px-4 py-2">Email</th> - <th class="px-4 py-2">Permissions</th> - <th class="px-4 py-2">Banned?</th> - <th class="px-4 py-2">Actions</th> - </tr> - </thead> - <tbody> - <?php foreach ($all_users as $user): ?> - <tr> - <td class="px-4 py-2 border"><?= $user->username ?></td> - <td class="px-4 py-2 border"><?= $user->email ?></td> - <td class="px-4 py-2 border">[<?= implode( - ', ', - $user->permissions - ) ?>]</td> - <td class="px-4 py-2 border"><?= $user->isBanned() - ? 'Yes' - : 'No' ?></td> - <td class="px-4 py-2 border"> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( - 'user_force_pass_reset', - $user->username - ) ?>"><?= lang('User.forcePassReset') ?></a> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to( - $user->isBanned() ? 'user_unban' : 'user_ban', - $user->username - ) ?>"> - <?= $user->isBanned() - ? lang('User.unban') - : lang('User.ban') ?></a> - <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( - 'user_delete', - $user->username - ) ?>"><?= lang('User.delete') ?></a> - </td> - </tr> - <?php endforeach; ?> - </tbody> - </table> - <?php else: ?> - <div class="flex items-center"> - <p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p> - <a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to( - 'podcast_create' - ) ?>"><?= lang('Podcast.create_one') ?></a> - </div> - <?php endif; ?> -</div> +<table class="table-auto"> + <thead> + <tr> + <th class="px-4 py-2">Username</th> + <th class="px-4 py-2">Email</th> + <th class="px-4 py-2">Permissions</th> + <th class="px-4 py-2">Banned?</th> + <th class="px-4 py-2">Actions</th> + </tr> + </thead> + <tbody> + <?php foreach ($all_users as $user): ?> + <tr> + <td class="px-4 py-2 border"><?= $user->username ?></td> + <td class="px-4 py-2 border"><?= $user->email ?></td> + <td class="px-4 py-2 border">[<?= implode( + ', ', + $user->permissions + ) ?>]</td> + <td class="px-4 py-2 border"><?= $user->isBanned() + ? 'Yes' + : 'No' ?></td> + <td class="px-4 py-2 border"> + <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( + 'user_force_pass_reset', + $user->id + ) ?>"><?= lang('User.forcePassReset') ?></a> + <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to( + $user->isBanned() ? 'user_unban' : 'user_ban', + $user->id + ) ?>"> + <?= $user->isBanned() + ? lang('User.unban') + : lang('User.ban') ?></a> + <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( + 'user_delete', + $user->id + ) ?>"><?= lang('User.delete') ?></a> + </td> + </tr> + <?php endforeach; ?> + </tbody> +</table> <?= $this->endSection() ?> diff --git a/app/Views/auth/_layout.php b/app/Views/auth/_layout.php index 24d4aff6b3303464db23dba4672749c53ce3460a..80f4c4ecb7e80a19f3141edfa603bc8ffac6c950 100644 --- a/app/Views/auth/_layout.php +++ b/app/Views/auth/_layout.php @@ -17,9 +17,7 @@ ) ?></a> </header> <main class="w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg shadow"> - <div class="mb-4"> - <?= view('_message_block') ?> - </div> + <?= view('_message_block') ?> <?= $this->renderSection('content') ?> </main> <footer class="flex flex-col text-sm"> diff --git a/composer.json b/composer.json index 3c0f646cd0026f0e03ed50e1ebfcb56a1387d852..ae8d398d9ac9549b48e1b165a72bd1db476a3644 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "license": "AGPL-3.0-or-later", "require": { "php": ">=7.2", - "codeigniter4/framework": "^4", + "codeigniter4/framework": "4.0.3", "james-heinrich/getid3": "~2.0.0-dev", "whichbrowser/parser": "^2.0", "geoip2/geoip2": "~2.0", diff --git a/composer.lock b/composer.lock index 16d8fd9a7ca1516163dac486b421b9d302a82c49..93f15e074ab82f13a3ff4589914436e0357f0439 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": "a03d5be6665057254fa301cada96586e", + "content-hash": "e494a281a4c6a239790ea930d05764e2", "packages": [ { "name": "codeigniter4/framework", @@ -1158,33 +1158,33 @@ }, { "name": "phpspec/prophecy", - "version": "v1.10.3", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -1217,7 +1217,7 @@ "spy", "stub" ], - "time": "2020-03-05T15:02:03+00:00" + "time": "2020-07-08T12:44:21+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2181,16 +2181,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.17.1", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d" + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", - "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", "shasum": "" }, "require": { @@ -2202,7 +2202,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2253,27 +2253,27 @@ "type": "tidelift" } ], - "time": "2020-06-06T08:46:27+00:00" + "time": "2020-07-14T12:35:20+00:00" }, { "name": "theseer/tokenizer", - "version": "1.1.3", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + "reference": "75a63c33a8577608444246075ea0af0d052e452a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -2293,24 +2293,30 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" }, { "name": "webmozart/assert", - "version": "1.9.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "9dc4f203e36f2b486149058bade43c851dd97451" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", - "reference": "9dc4f203e36f2b486149058bade43c851dd97451", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", + "php": "^5.3.3 || ^7.0 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -2342,7 +2348,7 @@ "check", "validate" ], - "time": "2020-06-16T10:16:42+00:00" + "time": "2020-07-08T17:02:28+00:00" } ], "aliases": [],