feat: add user permissions and basic groups to handle authorizations

- add AuthSeeder to bootstrap authorization data and remove UserSeeder
- create a superadmin group having all authorizations
- refactor routes and controller methods to separate get and post requests
- refactor admin views with a title section in layout
- add contributors section to podcasts to manage contributions (add, edit roles and remove)

closes #3, #18
This commit is contained in:
Yassine Doghri 2020-07-16 10:08:23 +00:00
parent c63a077618
commit d58e51874a
42 changed files with 1431 additions and 583 deletions

View File

@ -36,11 +36,12 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}');
// route since we don't have to scan directories. // route since we don't have to scan directories.
$routes->get('/', 'Home::index', ['as' => 'home']); $routes->get('/', 'Home::index', ['as' => 'home']);
// Public routes
$routes->group('@(:podcastName)', function ($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->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
$routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [ $routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [
'as' => 'episode', 'as' => 'episode',
]); ]);
}); });
@ -51,74 +52,119 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
]); ]);
// Show the Unknown UserAgents // Show the Unknown UserAgents
$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents'); $routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1'); $routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
// Admin area // Admin area
$routes->group( $routes->group(
config('App')->adminGateway, config('App')->adminGateway,
['namespace' => 'App\Controllers\Admin'], ['namespace' => 'App\Controllers\Admin'],
function ($routes) { function ($routes) {
$routes->add('/', 'Home', [ $routes->get('/', 'Home', [
'as' => 'admin', 'as' => 'admin',
]); ]);
$routes->add('new-podcast', 'Podcast::create', [ $routes->get('my-podcasts', 'Podcast::myPodcasts', [
'as' => 'podcast_create', '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) { // Use ids in admin area to help permission and group lookups
$routes->add('edit', 'Podcast::edit/$1', [ $routes->group('podcasts/(:num)', function ($routes) {
$routes->get('edit', 'Podcast::edit/$1', [
'as' => 'podcast_edit', 'as' => 'podcast_edit',
]); ]);
$routes->post('edit', 'Podcast::attemptEdit/$1');
$routes->add('delete', 'Podcast::delete/$1', [ $routes->add('delete', 'Podcast::delete/$1', [
'as' => 'podcast_delete', 'as' => 'podcast_delete',
]); ]);
$routes->add('new-episode', 'Episode::create/$1', [ // Podcast episodes
'as' => 'episode_create', $routes->get('episodes', 'Episode::list/$1', [
]);
$routes->add('episodes', 'Episode::list/$1', [
'as' => 'episode_list', 'as' => 'episode_list',
]); ]);
$routes->get('new-episode', 'Episode::create/$1', [
'as' => 'episode_create',
]);
$routes->post('new-episode', 'Episode::attemptCreate/$1');
$routes->add( $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
'episodes/(:episodeSlug)/edit', 'as' => 'episode_edit',
'Episode::edit/$1/$2', ]);
$routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2');
$routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [
'as' => 'episode_delete',
]);
// 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( $routes->add(
'episodes/(:episodeSlug)/delete', 'contributors/(:num)/remove',
'Episode::delete/$1/$2', 'Contributor::remove/$1/$2',
[ ['as' => 'contributor_remove']
'as' => 'episode_delete',
]
); );
}); });
// Users // Users
$routes->add('users', 'User::list', ['as' => 'user_list']); $routes->get('users', 'User::list', [
$routes->add('new-user', 'User::create', ['as' => 'user_create']); 'as' => 'user_list',
'filter' => 'permission:users-list',
$routes->add('users/@(:any)/ban', 'User::ban/$1', [
'as' => 'user_ban',
]); ]);
$routes->add('users/@(:any)/unban', 'User::unBan/$1', [ $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/(:num)/ban', 'User::ban/$1', [
'as' => 'user_ban',
'filter' => 'permission:users-manage_bans',
]);
$routes->add('users/(:num)/unban', 'User::unBan/$1', [
'as' => 'user_unban', 'as' => 'user_unban',
'filter' => 'permission:users-manage_bans',
]); ]);
$routes->add( $routes->add(
'users/@(:any)/force-pass-reset', 'users/(:num)/force-pass-reset',
'User::forcePassReset/$1', 'User::forcePassReset/$1',
[ [
'as' => 'user_force_pass_reset', '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', 'as' => 'user_delete',
'filter' => 'permission:users-delete',
]); ]);
// My account // My account

View File

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

View File

@ -17,23 +17,52 @@ class Episode extends BaseController
public function _remap($method, ...$params) 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(); $podcast_model = new PodcastModel();
$this->podcast = $podcast_model->where('name', $params[0])->first(); $this->podcast = $podcast_model->find($params[0]);
if (count($params) > 1) { if (count($params) > 1) {
$episode_model = new EpisodeModel(); $episode_model = new EpisodeModel();
if ( if (
!($episode = $episode_model !($this->episode = $episode_model
->where([ ->where([
'podcast_id' => $this->podcast->id, 'id' => $params[1],
'slug' => $params[1], 'podcast_id' => $params[0],
]) ])
->first()) ->first())
) { ) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
} }
$this->episode = $episode;
} }
return $this->$method(); return $this->$method();
@ -41,13 +70,8 @@ class Episode extends BaseController
public function list() public function list()
{ {
$episode_model = new EpisodeModel();
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'all_podcast_episodes' => $episode_model
->where('podcast_id', $this->podcast->id)
->find(),
]; ];
return view('admin/episode/list', $data); return view('admin/episode/list', $data);
@ -57,105 +81,118 @@ class Episode extends BaseController
{ {
helper(['form']); helper(['form']);
if ( $data = [
!$this->validate([ 'podcast' => $this->podcast,
'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); 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,
]);
$episode_model = new EpisodeModel(); public function attemptCreate()
$episode_model->save($new_episode); {
$rules = [
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
];
return redirect()->route('episode_list', [$this->podcast->name]); 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() public function edit()
{ {
helper(['form']); helper(['form']);
if ( $data = [
!$this->validate([ 'podcast' => $this->podcast,
'enclosure' => 'episode' => $this->episode,
'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); 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'); public function attemptEdit()
if ($enclosure->isValid()) { {
$this->episode->enclosure = $this->request->getFile( $rules = [
'enclosure' 'enclosure' =>
); 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
} 'image' =>
$image = $this->request->getFile('image'); 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
if ($image) { ];
$this->episode->image = $this->request->getFile('image');
}
$episode_model = new EpisodeModel(); if (!$this->validate($rules)) {
$episode_model->save($this->episode); return redirect()
->back()
return redirect()->route('episode_list', [$this->podcast->name]); ->withInput()
->with('errors', $this->validator->getErrors());
} }
$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() public function delete()
@ -163,6 +200,6 @@ class Episode extends BaseController
$episode_model = new EpisodeModel(); $episode_model = new EpisodeModel();
$episode_model->delete($this->episode->id); $episode_model->delete($this->episode->id);
return redirect()->route('episode_list', [$this->podcast->name]); return redirect()->route('episode_list', [$this->podcast->id]);
} }
} }

View File

@ -6,7 +6,6 @@
*/ */
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\UserPodcast;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\LanguageModel; use App\Models\LanguageModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
@ -18,18 +17,59 @@ class Podcast extends BaseController
public function _remap($method, ...$params) public function _remap($method, ...$params)
{ {
if (count($params) > 0) { 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(); $podcast_model = new PodcastModel();
if ( if (!($this->podcast = $podcast_model->find($params[0]))) {
!($podcast = $podcast_model->where('name', $params[0])->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
} }
$this->podcast = $podcast;
} }
return $this->$method(); return $this->$method();
} }
public function myPodcasts()
{
$data = [
'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
];
return view('admin/podcast/list', $data);
}
public function list() public function list()
{ {
$podcast_model = new PodcastModel(); $podcast_model = new PodcastModel();
@ -42,133 +82,141 @@ class Podcast extends BaseController
public function create() public function create()
{ {
helper(['form', 'misc']); helper(['form', 'misc']);
$podcast_model = new PodcastModel();
if ( $languageModel = new LanguageModel();
!$this->validate([ $categoryModel = new CategoryModel();
'title' => 'required', $data = [
'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]', 'languages' => $languageModel->findAll(),
'description' => 'required|max_length[4000]', 'categories' => $categoryModel->findAll(),
'image' => 'browser_lang' => get_browser_language(
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]', $this->request->getServer('HTTP_ACCEPT_LANGUAGE')
'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); 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(); public function attemptCreate()
{
$rules = [
'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
];
$db->transStart(); if (!$this->validate($rules)) {
return redirect()
$new_podcast_id = $podcast_model->insert($podcast, true); ->back()
->withInput()
$user_podcast_model = new \App\Models\UserPodcastModel(); ->with('errors', $this->validator->getErrors());
$user_podcast_model->save([
'user_id' => user()->id,
'podcast_id' => $new_podcast_id,
]);
$db->transComplete();
return redirect()->route('podcast_list', [$podcast->name]);
} }
$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();
$db->transStart();
if (!($new_podcast_id = $podcast_model->insert($podcast, true))) {
$db->transComplete();
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() public function edit()
{ {
helper(['form', 'misc']); helper('form');
if ( $languageModel = new LanguageModel();
!$this->validate([ $categoryModel = new CategoryModel();
'title' => 'required', $data = [
'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]', 'podcast' => $this->podcast,
'description' => 'required|max_length[4000]', 'languages' => $languageModel->findAll(),
'image' => 'categories' => $categoryModel->findAll(),
'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); 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'
);
$image = $this->request->getFile('image'); public function attemptEdit()
if ($image->isValid()) { {
$this->podcast->image = $this->request->getFile('image'); $rules = [
} 'image' =>
$this->podcast->language = $this->request->getVar('language'); 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
$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'
);
$podcast_model = new PodcastModel(); if (!$this->validate($rules)) {
$podcast_model->save($this->podcast); return redirect()
->back()
return redirect()->route('podcast_list', [$this->podcast->name]); ->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'
);
$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() public function delete()

View File

@ -17,12 +17,9 @@ class User extends BaseController
{ {
if (count($params) > 0) { if (count($params) > 0) {
$user_model = new UserModel(); $user_model = new UserModel();
if ( if (!($this->user = $user_model->find($params[0]))) {
!($user = $user_model->where('username', $params[0])->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
} }
$this->user = $user;
} }
return $this->$method(); return $this->$method();
@ -38,6 +35,11 @@ class User extends BaseController
} }
public function create() public function create()
{
echo view('admin/user/create');
}
public function attemptCreate()
{ {
$user_model = new UserModel(); $user_model = new UserModel();
@ -53,30 +55,33 @@ class User extends BaseController
); );
if (!$this->validate($rules)) { 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());
}
// Success!
return redirect() return redirect()
->route('user_list') ->back()
->with('message', lang('User.createSuccess')); ->withInput()
->with('errors', $this->validator->getErrors());
} }
// 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());
}
// Success!
return redirect()
->route('user_list')
->with('message', lang('User.createSuccess'));
} }
public function forcePassReset() public function forcePassReset()

View File

@ -24,7 +24,7 @@ class Episode extends BaseController
if (count($params) > 1) { if (count($params) > 1) {
$episode_model = new EpisodeModel(); $episode_model = new EpisodeModel();
if ( if (
!($episode = $episode_model !($this->episode = $episode_model
->where([ ->where([
'podcast_id' => $this->podcast->id, 'podcast_id' => $this->podcast->id,
'slug' => $params[1], 'slug' => $params[1],
@ -33,7 +33,6 @@ class Episode extends BaseController
) { ) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
} }
$this->episode = $episode;
} }
return $this->$method(); return $this->$method();

View File

@ -17,11 +17,12 @@ class Podcast extends BaseController
if (count($params) > 0) { if (count($params) > 0) {
$podcast_model = new PodcastModel(); $podcast_model = new PodcastModel();
if ( if (
!($podcast = $podcast_model->where('name', $params[0])->first()) !($this->podcast = $podcast_model
->where('name', $params[0])
->first())
) { ) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
} }
$this->podcast = $podcast;
} }
return $this->$method(); return $this->$method();

View File

@ -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.', '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, 'null' => true,
], ],
'owner_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'comment' => 'The podcast owner.',
],
'owner_name' => [ 'owner_name' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 1024, 'constraint' => 1024,
'owner_name' => 'comment' =>
'The podcast owner name. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts.', 'The podcast owner name. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts.',
'null' => true, 'null' => true,
], ],
'owner_email' => [ 'owner_email' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 1024, 'constraint' => 1024,
'owner_email' => 'comment' =>
'The podcast owner email address. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts. Please make sure the email address is active and monitored.', 'The podcast owner email address. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts. Please make sure the email address is active and monitored.',
'null' => true, 'null' => true,
], ],
@ -147,6 +153,7 @@ class AddPodcasts extends Migration
], ],
]); ]);
$this->forge->addKey('id', true); $this->forge->addKey('id', true);
$this->forge->addForeignKey('owner_id', 'users', 'id');
$this->forge->createTable('podcasts'); $this->forge->createTable('podcasts');
} }

View File

@ -17,12 +17,6 @@ class AddUsersPodcasts extends Migration
public function up() public function up()
{ {
$this->forge->addField([ $this->forge->addField([
'id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'auto_increment' => true,
],
'user_id' => [ 'user_id' => [
'type' => 'INT', 'type' => 'INT',
'constraint' => 11, 'constraint' => 11,
@ -34,8 +28,7 @@ class AddUsersPodcasts extends Migration
'unsigned' => true, 'unsigned' => true,
], ],
]); ]);
$this->forge->addPrimaryKey('id'); $this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addUniqueKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id'); $this->forge->addForeignKey('user_id', 'users', 'id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->createTable('users_podcasts'); $this->forge->createTable('users_podcasts');

View File

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

View File

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

View File

@ -9,6 +9,7 @@ namespace App\Entities;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use CodeIgniter\Entity; use CodeIgniter\Entity;
use Myth\Auth\Models\UserModel;
class Podcast extends Entity class Podcast extends Entity
{ {
@ -17,6 +18,8 @@ class Podcast extends Entity
protected $image_media_path; protected $image_media_path;
protected $image_url; protected $image_url;
protected $episodes; protected $episodes;
protected $owner;
protected $contributors;
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
@ -29,6 +32,7 @@ class Podcast extends Entity
'explicit' => 'boolean', 'explicit' => 'boolean',
'author_name' => '?string', 'author_name' => '?string',
'author_email' => '?string', 'author_email' => '?string',
'owner_id' => 'integer',
'owner_name' => '?string', 'owner_name' => '?string',
'owner_email' => '?string', 'owner_email' => '?string',
'type' => '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->episodes = (new EpisodeModel())->getPodcastEpisodes(
$this->id $this->id
); );
@ -100,4 +104,40 @@ class Podcast extends Entity
return $this->episodes; 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();
}
} }

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@
return [ return [
'all_podcast_episodes' => 'All podcast episodes', 'all_podcast_episodes' => 'All podcast episodes',
'create_one' => 'Add a new one',
'back_to_podcast' => 'Go back to podcast', 'back_to_podcast' => 'Go back to podcast',
'edit' => 'Edit', 'edit' => 'Edit',
'delete' => 'Delete', 'delete' => 'Delete',

View File

@ -7,5 +7,6 @@
return [ return [
'passwordChangeSuccess' => 'Password has been successfully changed!', 'passwordChangeSuccess' => 'Password has been successfully changed!',
'changePassword' => 'Change my password' 'changePassword' => 'Change my password',
'info' => 'My account info'
]; ];

View File

@ -8,13 +8,13 @@
return [ return [
'all_podcasts' => 'All podcasts', 'all_podcasts' => 'All podcasts',
'no_podcast' => 'No podcast found!', 'no_podcast' => 'No podcast found!',
'create_one' => 'Add a new one',
'create' => 'Create a Podcast', 'create' => 'Create a Podcast',
'new_episode' => 'New Episode', 'new_episode' => 'New Episode',
'feed' => 'RSS feed', 'feed' => 'RSS feed',
'edit' => 'Edit', 'edit' => 'Edit',
'delete' => 'Delete', 'delete' => 'Delete',
'see_episodes' => 'See episodes', 'see_episodes' => 'See episodes',
'see_contributors' => 'See contributors',
'goto_page' => 'Go to page', 'goto_page' => 'Go to page',
'form' => [ 'form' => [
'title' => 'Title', 'title' => 'Title',

View File

@ -10,11 +10,13 @@ return [
'forcePassResetSuccess' => 'The user will be prompted with a password reset during his next login attempt.', 'forcePassResetSuccess' => 'The user will be prompted with a password reset during his next login attempt.',
'banSuccess' => 'User has been banned.', 'banSuccess' => 'User has been banned.',
'unbanSuccess' => 'User has been unbanned.', 'unbanSuccess' => 'User has been unbanned.',
'deleteSuccess' => 'User has been deleted.',
'forcePassReset' => 'Force pass reset', 'forcePassReset' => 'Force pass reset',
'ban' => 'Ban', 'ban' => 'Ban',
'unban' => 'Unban', 'unban' => 'Unban',
'delete' => 'Delete', 'delete' => 'Delete',
'create' => 'Create a user', 'create' => 'Create a user',
'all_users' => 'All users',
'form' => [ 'form' => [
'email' => 'Email', 'email' => 'Email',
'username' => 'Username', 'username' => 'Username',

View File

@ -19,11 +19,8 @@ class EpisodeModel extends Model
'title', 'title',
'slug', 'slug',
'enclosure_uri', 'enclosure_uri',
'enclosure_length',
'enclosure_type',
'pub_date', 'pub_date',
'description', 'description',
'duration',
'image_uri', 'image_uri',
'explicit', 'explicit',
'number', 'number',
@ -39,6 +36,21 @@ class EpisodeModel extends Model
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $useTimestamps = 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 $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache']; protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache'];

View File

@ -8,6 +8,8 @@
namespace App\Models; namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
use Myth\Auth\Authorization\GroupModel;
use Myth\Auth\Config\Services;
class PodcastModel extends Model class PodcastModel extends Model
{ {
@ -26,6 +28,7 @@ class PodcastModel extends Model
'explicit', 'explicit',
'author_name', 'author_name',
'author_email', 'author_email',
'owner_id',
'owner_name', 'owner_name',
'owner_email', 'owner_email',
'type', 'type',
@ -40,8 +43,60 @@ class PodcastModel extends Model
protected $useTimestamps = true; 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 $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) protected function clearCache(array $data)
{ {
@ -56,5 +111,95 @@ class PodcastModel extends Model
// foreach ($podcast->episodes as $episode) { // foreach ($podcast->episodes as $episode) {
// $cache->delete(md5($episode->link)); // $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;
} }
} }

View File

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

View File

@ -1,17 +1,17 @@
<?php if (session()->has('message')): ?> <?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') ?> <?= session('message') ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (session()->has('error')): ?> <?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') ?> <?= session('error') ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (session()->has('errors')): ?> <?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): ?> <?php foreach (session('errors') as $error): ?>
<li><?= $error ?></li> <li><?= $error ?></li>
<?php endforeach; ?> <?php endforeach; ?>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <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="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"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
@ -12,9 +12,12 @@
<body class="flex flex-col min-h-screen mx-auto"> <body class="flex flex-col min-h-screen mx-auto">
<header class="text-white bg-gray-900 border-b"> <header class="text-white bg-gray-900 border-b">
<div class="flex items-center justify-between px-4 py-4 mx-auto"> <div class="flex items-center px-4 py-4 mx-auto">
<a href="<?= route_to('home') ?>" class="text-xl">Castopod Admin</a> <a href="<?= route_to('admin') ?>" class="text-xl">Castopod Admin</a>
<nav> <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> <span class="mr-2">Welcome, <?= user()->username ?></span>
<a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to( <a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to(
'logout' 'logout'
@ -25,9 +28,8 @@
<div class="flex flex-1"> <div class="flex flex-1">
<?= view('admin/_sidenav') ?> <?= view('admin/_sidenav') ?>
<main class="container flex-1 px-4 py-6 mx-auto"> <main class="container flex-1 px-4 py-6 mx-auto">
<div class="mb-4"> <h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1>
<?= view('_message_block') ?> <?= view('_message_block') ?>
</div>
<?= $this->renderSection('content') ?> <?= $this->renderSection('content') ?>
</main> </main>
</div> </div>

View File

@ -8,6 +8,11 @@
<div class="mb-4"> <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> <span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Podcasts</span>
<ul> <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> <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( <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' 'podcast_list'

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('content') ?> <?= $this->section(
'title'
<h1 class="text-2xl">Welcome to the admin dashboard!</h1> ) ?>Welcome to the admin dashboard!<?= $this->endSection() ?>
<?= $this->endSection() ?>

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Episode.create') ?></h1> <?= form_open_multipart(route_to('episode_create', $podcast->id), [
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(route_to('episode_create', $podcast->name), [
'method' => 'post', 'method' => 'post',
'class' => 'flex flex-col max-w-md', 'class' => 'flex flex-col max-w-md',
]) ?> ]) ?>
@ -21,24 +20,30 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="title"><?= lang('Episode.form.title') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="slug"><?= lang('Episode.form.slug') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="description"><?= lang('Episode.form.description') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="pub_date"><?= lang('Episode.form.pub_date') ?></label> <label for="pub_date"><?= lang('Episode.form.pub_date') ?></label>
<input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date( <input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= old(
'Y-m-d' 'pub_date'
) ?>" /> ) || date('Y-m-d') ?>" />
</div> </div>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
@ -50,16 +55,22 @@
<label for="episode_number"><?= lang( <label for="episode_number"><?= lang(
'Episode.form.episode_number' 'Episode.form.episode_number'
) ?></label> ) ?></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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="season_number"><?= lang('Episode.form.season_number') ?></label> <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>
<div class="inline-flex items-center mb-4"> <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( <label for="explicit" class="pl-2"><?= lang(
'Episode.form.explicit' 'Episode.form.explicit'
) ?></label> ) ?></label>
@ -67,32 +78,45 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label> <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>
<fieldset class="flex flex-col mb-4"> <fieldset class="flex flex-col mb-4">
<legend><?= lang('Episode.form.type.label') ?></legend> <legend><?= lang('Episode.form.type.label') ?></legend>
<label for="full" class="inline-flex items-center"> <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> <span class="ml-2"><?= lang('Episode.form.type.full') ?></span>
</label> </label>
<label for="trailer" class="inline-flex items-center"> <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> <span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>
</label> </label>
<label for="bonus" class="inline-flex items-center"> <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> <span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>
</label> </label>
</fieldset> </fieldset>
<div class="inline-flex items-center mb-4"> <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> <label for="block" class="pl-2"><?= lang('Episode.form.block') ?></label>
</div> </div>

View File

@ -1,20 +1,16 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Episode.edit') ?></h1> <?= form_open_multipart(route_to('episode_edit', $podcast->id, $episode->id), [
'method' => 'post',
<div class="mb-8"> 'class' => 'flex flex-col max-w-md',
<?= \Config\Services::validation()->listErrors() ?> ]) ?>
</div>
<?= form_open_multipart(
route_to('episode_edit', $podcast->name, $episode->slug),
[
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]
) ?>
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">

View File

@ -1,21 +1,29 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $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"> <div class="flex flex-col py-4">
<h1 class="mb-4 text-xl"><?= lang( <?php if ($podcast->episodes): ?>
'Episode.all_podcast_episodes' <?php foreach ($podcast->episodes as $episode): ?>
) ?> (<?= count($all_podcast_episodes) ?>)</h1>
<?php if ($all_podcast_episodes): ?>
<?php foreach ($all_podcast_episodes as $episode): ?>
<article class="flex-col w-full max-w-lg p-4 mb-4 border shadow"> <article class="flex-col w-full max-w-lg p-4 mb-4 border shadow">
<div class="flex mb-2"> <div class="flex mb-2">
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" /> <img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<a href="<?= route_to( <a href="<?= route_to(
'episode_edit', 'episode_edit',
$podcast->name, $podcast->id,
$episode->slug $episode->id
) ?>"> ) ?>">
<h3 class="text-xl font-semibold"> <h3 class="text-xl font-semibold">
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span> <span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
@ -24,15 +32,15 @@
<p><?= $episode->description ?></p> <p><?= $episode->description ?></p>
</a> </a>
<audio controls class="mt-auto" preload="none"> <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. Your browser does not support the audio tag.
</audio> </audio>
</div> </div>
</div> </div>
<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( <a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
'episode_edit', 'episode_edit',
$podcast->name, $podcast->id,
$episode->slug $episode->id
) ?>"><?= lang('Episode.edit') ?></a> ) ?>"><?= lang('Episode.edit') ?></a>
<a href="<?= route_to( <a href="<?= route_to(
'episode', 'episode',
@ -43,21 +51,15 @@
) ?></a> ) ?></a>
<a href="<?= route_to( <a href="<?= route_to(
'episode_delete', 'episode_delete',
$podcast->name, $podcast->id,
$episode->slug $episode->id
) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang( ) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
'Episode.delete' 'Episode.delete'
) ?></a> ) ?></a>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>
<div class="flex items-center"> <p class="italic"><?= lang('Podcast.no_episode') ?></p>
<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>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@ -1,8 +1,11 @@
<?= $this->extend('admin/_layout') ?> <?= $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( <form action="<?= route_to(
'myAccount_changePassword' 'myAccount_changePassword'

View File

@ -1,5 +1,10 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('MyAccount.info') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Podcast.create') ?></h1> <?= form_open_multipart(route_to('podcast_create'), [
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(base_url(route_to('podcast_create')), [
'method' => 'post', 'method' => 'post',
'class' => 'flex flex-col max-w-md', 'class' => 'flex flex-col max-w-md',
]) ?> ]) ?>
@ -16,24 +15,32 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="title"><?= lang('Podcast.form.title') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="name"><?= lang('Podcast.form.name') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="description"><?= lang('Podcast.form.description') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="episode_description_footer"><?= lang( <label for="episode_description_footer"><?= lang(
'Podcast.form.episode_description_footer' 'Podcast.form.episode_description_footer'
) ?></label> ) ?></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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
@ -45,9 +52,15 @@
<label for="language"><?= lang('Podcast.form.language') ?></label> <label for="language"><?= lang('Podcast.form.language') ?></label>
<select id="language" name="language" autocomplete="off" class="form-select" required> <select id="language" name="language" autocomplete="off" class="form-select" required>
<?php foreach ($languages as $language): ?> <?php foreach ($languages as $language): ?>
<option <?= $language->code == $browser_lang <option value="<?= $language->code ?>"
? "selected='selected'" <?php if (
: '' ?> value="<?= $language->code ?>"> old('language') == $language->code
): ?> selected <?php endif; ?>
<?php if (
!old('language') &&
$language->code == $browser_lang
): ?> selected <?php endif; ?>
>
<?= $language->native_name ?> <?= $language->native_name ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
@ -58,16 +71,20 @@
<label for="category"><?= lang('Podcast.form.category') ?></label> <label for="category"><?= lang('Podcast.form.category') ?></label>
<select id="category" name="category" class="form-select" required> <select id="category" name="category" class="form-select" required>
<?php foreach ($categories as $category): ?> <?php foreach ($categories as $category): ?>
<option value="<?= $category->code ?>"><?= lang( <option value="<?= $category->code ?>"
'Podcast.category_options.' . $category->code <?php if (
) ?> old('category') == $category->code
): ?> selected <?php endif; ?>
><?= lang('Podcast.category_options.' . $category->code) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="inline-flex items-center mb-4"> <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( <label for="explicit" class="pl-2"><?= lang(
'Podcast.form.explicit' 'Podcast.form.explicit'
) ?></label> ) ?></label>
@ -75,48 +92,67 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="owner_name"><?= lang('Podcast.form.owner_name') ?></label> <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>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="owner_email"><?= lang('Podcast.form.owner_email') ?></label> <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> </div>
<fieldset class="flex flex-col mb-4"> <fieldset class="flex flex-col mb-4">
<legend><?= lang('Podcast.form.type.label') ?></legend> <legend><?= lang('Podcast.form.type.label') ?></legend>
<label for="episodic" class="inline-flex items-center"> <label for="episodic" class="inline-flex items-center">
<input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required checked /> <input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required <?php if (
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span> !old('type') ||
old('type') == 'episodic'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
</label> </label>
<label for="serial" class="inline-flex items-center"> <label for="serial" class="inline-flex items-center">
<input type="radio" class="form-radio" value="serial" id="serial" name="type" required /> <input type="radio" class="form-radio" value="serial" id="serial" name="type" required <?php if (
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span> old('type') == 'serial'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
</label> </label>
</fieldset> </fieldset>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<label for="copyright"><?= lang('Podcast.form.copyright') ?></label> <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>
<div class="inline-flex items-center mb-4"> <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> <label for="block" class="pl-2"><?= lang('Podcast.form.block') ?></label>
</div> </div>
<div class="inline-flex items-center mb-4"> <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( <label for="complete" class="pl-2"><?= lang(
'Podcast.form.complete' 'Podcast.form.complete'
) ?></label> ) ?></label>

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Podcast.edit') ?></h1> <?= form_open_multipart(route_to('podcast_edit', $podcast->id), [
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(base_url(route_to('podcast_edit', $podcast->name)), [
'method' => 'post', 'method' => 'post',
'class' => 'flex flex-col max-w-md', 'class' => 'flex flex-col max-w-md',
]) ?> ]) ?>
@ -70,7 +69,9 @@
</div> </div>
<div class="inline-flex items-center mb-4"> <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( <label for="explicit" class="pl-2"><?= lang(
'Podcast.form.explicit' 'Podcast.form.explicit'
) ?></label> ) ?></label>
@ -123,7 +124,7 @@
<div class="inline-flex items-center mb-4"> <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"
<?= $podcast->block ? 'checked' : '' ?> /> <?= $podcast->complete ? 'checked' : '' ?> />
<label for="complete" class="pl-2"><?= lang( <label for="complete" class="pl-2"><?= lang(
'Podcast.form.complete' 'Podcast.form.complete'
) ?></label> ) ?></label>

View File

@ -1,10 +1,15 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.all_podcasts') ?> (<?= count($all_podcasts) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-2 text-xl"><?= lang('Podcast.all_podcasts') ?> (<?= count( <a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
$all_podcasts 'podcast_create'
) ?>)</h1> ) ?>"><?= lang('Podcast.create') ?></a>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<?php if ($all_podcasts): ?> <?php if ($all_podcasts): ?>
<?php foreach ($all_podcasts as $podcast): ?> <?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" /> <img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
<a href="<?= route_to( <a href="<?= route_to(
'episode_list', 'episode_list',
$podcast->name $podcast->id
) ?>" class="hover:underline"> ) ?>" class="hover:underline">
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2> <h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
</a> </a>
<p class="mb-4 text-gray-600">@<?= $podcast->name ?></p> <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( <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_edit',
$podcast->name $podcast->id
) ?>"><?= lang('Podcast.edit') ?></a> ) ?>"><?= 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( <a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to(
'episode_list', 'episode_list',
$podcast->name $podcast->id
) ?>"><?= lang('Podcast.see_episodes') ?></a> ) ?>"><?= 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( <a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
'podcast', 'podcast',
$podcast->name $podcast->name
) ?>"><?= lang('Podcast.goto_page') ?></a> ) ?>"><?= 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( <a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
'podcast_delete', 'podcast_delete',
$podcast->name $podcast->id
) ?>"><?= lang('Podcast.delete') ?></a> ) ?>"><?= lang('Podcast.delete') ?></a>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>
<div class="flex items-center"> <p class="italic"><?= lang('Podcast.no_podcast') ?></p>
<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; ?> <?php endif; ?>
</div> </div>

View File

@ -1,13 +1,12 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('User.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('User.create') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<form action="<?= route_to( <form action="<?= route_to(
'user_create' 'user_create'
) ?>" method="post" class="flex flex-col max-w-lg"> ) ?>" method="post" class="flex flex-col max-w-lg">

View File

@ -1,61 +1,55 @@
<?= $this->extend('admin/_layout') ?> <?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('User.all_users') ?> (<?= count($all_users) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="flex flex-wrap"> <table class="table-auto">
<?php if ($all_users): ?> <thead>
<table class="table-auto"> <tr>
<thead> <th class="px-4 py-2">Username</th>
<tr> <th class="px-4 py-2">Email</th>
<th class="px-4 py-2">Username</th> <th class="px-4 py-2">Permissions</th>
<th class="px-4 py-2">Email</th> <th class="px-4 py-2">Banned?</th>
<th class="px-4 py-2">Permissions</th> <th class="px-4 py-2">Actions</th>
<th class="px-4 py-2">Banned?</th> </tr>
<th class="px-4 py-2">Actions</th> </thead>
</tr> <tbody>
</thead> <?php foreach ($all_users as $user): ?>
<tbody> <tr>
<?php foreach ($all_users as $user): ?> <td class="px-4 py-2 border"><?= $user->username ?></td>
<tr> <td class="px-4 py-2 border"><?= $user->email ?></td>
<td class="px-4 py-2 border"><?= $user->username ?></td> <td class="px-4 py-2 border">[<?= implode(
<td class="px-4 py-2 border"><?= $user->email ?></td> ', ',
<td class="px-4 py-2 border">[<?= implode( $user->permissions
', ', ) ?>]</td>
$user->permissions <td class="px-4 py-2 border"><?= $user->isBanned()
) ?>]</td> ? 'Yes'
<td class="px-4 py-2 border"><?= $user->isBanned() : 'No' ?></td>
? 'Yes' <td class="px-4 py-2 border">
: 'No' ?></td> <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
<td class="px-4 py-2 border"> 'user_force_pass_reset',
<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->id
'user_force_pass_reset', ) ?>"><?= lang('User.forcePassReset') ?></a>
$user->username <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
) ?>"><?= lang('User.forcePassReset') ?></a> $user->isBanned() ? 'user_unban' : 'user_ban',
<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->id
$user->isBanned() ? 'user_unban' : 'user_ban', ) ?>">
$user->username <?= $user->isBanned()
) ?>"> ? lang('User.unban')
<?= $user->isBanned() : lang('User.ban') ?></a>
? lang('User.unban') <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
: lang('User.ban') ?></a> 'user_delete',
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( $user->id
'user_delete', ) ?>"><?= lang('User.delete') ?></a>
$user->username </td>
) ?>"><?= lang('User.delete') ?></a> </tr>
</td> <?php endforeach; ?>
</tr> </tbody>
<?php endforeach; ?> </table>
</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>
<?= $this->endSection() <?= $this->endSection()
?> ?>

View File

@ -17,9 +17,7 @@
) ?></a> ) ?></a>
</header> </header>
<main class="w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg shadow"> <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') ?>
<?= view('_message_block') ?>
</div>
<?= $this->renderSection('content') ?> <?= $this->renderSection('content') ?>
</main> </main>
<footer class="flex flex-col text-sm"> <footer class="flex flex-col text-sm">

View File

@ -6,7 +6,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
"php": ">=7.2", "php": ">=7.2",
"codeigniter4/framework": "^4", "codeigniter4/framework": "4.0.3",
"james-heinrich/getid3": "~2.0.0-dev", "james-heinrich/getid3": "~2.0.0-dev",
"whichbrowser/parser": "^2.0", "whichbrowser/parser": "^2.0",
"geoip2/geoip2": "~2.0", "geoip2/geoip2": "~2.0",

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a03d5be6665057254fa301cada96586e", "content-hash": "e494a281a4c6a239790ea930d05764e2",
"packages": [ "packages": [
{ {
"name": "codeigniter4/framework", "name": "codeigniter4/framework",
@ -1158,33 +1158,33 @@
}, },
{ {
"name": "phpspec/prophecy", "name": "phpspec/prophecy",
"version": "v1.10.3", "version": "1.11.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpspec/prophecy.git", "url": "https://github.com/phpspec/prophecy.git",
"reference": "451c3cd1418cf640de218914901e51b064abb093" "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
"reference": "451c3cd1418cf640de218914901e51b064abb093", "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/instantiator": "^1.0.2", "doctrine/instantiator": "^1.2",
"php": "^5.3|^7.0", "php": "^7.2",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^5.0",
"sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", "sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" "sebastian/recursion-context": "^3.0 || ^4.0"
}, },
"require-dev": { "require-dev": {
"phpspec/phpspec": "^2.5 || ^3.2", "phpspec/phpspec": "^6.0",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" "phpunit/phpunit": "^8.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.10.x-dev" "dev-master": "1.11.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -1217,7 +1217,7 @@
"spy", "spy",
"stub" "stub"
], ],
"time": "2020-03-05T15:02:03+00:00" "time": "2020-07-08T12:44:21+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -2181,16 +2181,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.17.1", "version": "v1.18.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d" "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2202,7 +2202,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.17-dev" "dev-master": "1.18-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@ -2253,27 +2253,27 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-06-06T08:46:27+00:00" "time": "2020-07-14T12:35:20+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.1.3", "version": "1.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/tokenizer.git", "url": "https://github.com/theseer/tokenizer.git",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" "reference": "75a63c33a8577608444246075ea0af0d052e452a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "reference": "75a63c33a8577608444246075ea0af0d052e452a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-tokenizer": "*", "ext-tokenizer": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"php": "^7.0" "php": "^7.2 || ^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -2293,24 +2293,30 @@
} }
], ],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "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", "name": "webmozart/assert",
"version": "1.9.0", "version": "1.9.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/webmozart/assert.git", "url": "https://github.com/webmozart/assert.git",
"reference": "9dc4f203e36f2b486149058bade43c851dd97451" "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "9dc4f203e36f2b486149058bade43c851dd97451", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^5.3.3 || ^7.0", "php": "^5.3.3 || ^7.0 || ^8.0",
"symfony/polyfill-ctype": "^1.8" "symfony/polyfill-ctype": "^1.8"
}, },
"conflict": { "conflict": {
@ -2342,7 +2348,7 @@
"check", "check",
"validate" "validate"
], ],
"time": "2020-06-16T10:16:42+00:00" "time": "2020-07-08T17:02:28+00:00"
} }
], ],
"aliases": [], "aliases": [],