Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/podlove-podcasting-plugin-for-wordpress/.php-cs-fixer.dist.php
+++ b/podlove-podcasting-plugin-for-wordpress/.php-cs-fixer.dist.php
@@ -1,24 +0,0 @@
-<?php
-
-$finder = PhpCsFixerFinder::create()
- ->exclude('vendor')
- ->in(__DIR__)
-;
-
-$config = new PhpCsFixerConfig();
-
-$c = $config->setRules([
- '@PSR2' => true,
- '@PhpCsFixer' => true,
- 'yoda_style' => false,
- 'fully_qualified_strict_types' => false,
- 'array_syntax' => ['syntax' => 'short'],
- 'trailing_comma_in_multiline' => false,
- 'no_trailing_comma_in_singleline_array' => true,
- 'blank_line_before_statement' => ['statements' => ['break', 'continue', 'declare', 'default', 'return', 'throw', 'try']],
- 'visibility_required' => ['elements' => ['method', 'property']]
-])
- ->setFinder($finder)
-;
-
-return $c;
--- a/podlove-podcasting-plugin-for-wordpress/config/php-scoper/matomo.inc.php
+++ b/podlove-podcasting-plugin-for-wordpress/config/php-scoper/matomo.inc.php
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use IsolatedSymfonyComponentFinderFinder;
-
-return [
- 'finders' => [
- Finder::create()->files()->in('vendor/matomo/*')->name(['*.php', '*.yml', 'LICENSE', 'composer.json']),
- Finder::create()->files()->in('vendor/mustangostang/*')->name(['*.php', 'LICENSE', 'composer.json']),
- ],
- 'patchers' => [
- function (string $filePath, string $prefix, string $content): string {
- $content = str_replace(
- 'class_exists('DeviceDetector',
- 'class_exists(''.$prefix.'\DeviceDetector',
- $content
- );
-
- $content = str_replace(
- '$className = 'DeviceDetector',
- '$className = ''.$prefix.'\DeviceDetector',
- $content
- );
-
- // hack: remove faulty escaping in regex; not sure why php-scoper even touch this line
- if (stristr($filePath, 'AbstractParser.php') || stristr($filePath, 'DeviceDetector.php') || stristr($filePath, 'ShellTv.php') || stristr($filePath, 'HbbTV.php') || stristr($filePath, 'Version.php')) {
- $content = str_replace('\\', '\', $content);
- }
-
- return $content.'';
- }
- ]
-];
--- a/podlove-podcasting-plugin-for-wordpress/config/php-scoper/monolog.inc.php
+++ b/podlove-podcasting-plugin-for-wordpress/config/php-scoper/monolog.inc.php
@@ -1,13 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use IsolatedSymfonyComponentFinderFinder;
-
-return [
- 'finders' => [
- Finder::create()->files()->in('vendor/monolog/*')->name(['*.php', 'LICENSE', 'composer.json']),
- ],
- 'patchers' => [
- ]
-];
--- a/podlove-podcasting-plugin-for-wordpress/config/php-scoper/piwik.inc.php
+++ b/podlove-podcasting-plugin-for-wordpress/config/php-scoper/piwik.inc.php
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use IsolatedSymfonyComponentFinderFinder;
-
-return [
- 'finders' => [
- Finder::create()->files()->in('vendor/piwik/*')->name(['*.php', '*.yml', 'LICENSE', 'composer.json']),
- Finder::create()->files()->in('vendor/mustangostang/*')->name(['*.php', 'LICENSE', 'composer.json']),
- ],
- 'patchers' => [
- function (string $filePath, string $prefix, string $content): string {
- $content = str_replace(
- 'class_exists('DeviceDetector',
- 'class_exists(''.$prefix.'\DeviceDetector',
- $content
- );
-
- $content = str_replace(
- '$className = 'DeviceDetector',
- '$className = ''.$prefix.'\DeviceDetector',
- $content
- );
-
- return $content.'';
- }
- ]
-];
--- a/podlove-podcasting-plugin-for-wordpress/config/php-scoper/psr.inc.php
+++ b/podlove-podcasting-plugin-for-wordpress/config/php-scoper/psr.inc.php
@@ -1,13 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use IsolatedSymfonyComponentFinderFinder;
-
-return [
- 'finders' => [
- Finder::create()->files()->in('vendor/psr/*')->name(['*.php', 'LICENSE', 'composer.json']),
- ],
- 'patchers' => [
- ]
-];
--- a/podlove-podcasting-plugin-for-wordpress/config/php-scoper/twig.inc.php
+++ b/podlove-podcasting-plugin-for-wordpress/config/php-scoper/twig.inc.php
@@ -1,120 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use IsolatedSymfonyComponentFinderFinder;
-
-return [
- // By default when running php-scoper add-prefix, it will prefix all relevant code found in the current working
- // directory. You can however define which files should be scoped by defining a collection of Finders in the
- // following configuration key.
- //
- // For more see: https://github.com/humbug/php-scoper#finders-and-paths
- 'finders' => [
- Finder::create()->files()->in('vendor/twig/*')->name(['*.php', 'LICENSE', 'composer.json']),
- ],
-
- 'patchers' => [
- function (string $filePath, string $prefix, string $content): string {
- // suppress warnings for class_alias
- $content = preg_replace('/(\class_alias)/', '@${1}', $content);
-
- if (stristr($filePath, 'CoreExtension.php') || stristr($filePath, 'EscaperExtension.php') || stristr($filePath, 'DebugExtension.php')) {
- $pattern = '/TwigFilter(('[^']+'),s+'(_?twig[^']+)'/';
- $content = preg_replace_callback(
- $pattern,
- function ($matches) use ($prefix) {
- return 'TwigFilter('.$matches[1].', ''.$prefix.'\'.$matches[2].''';
- },
- $content
- );
-
- $pattern = '/TwigFunction(('[^']+'),s+'(twig[^']+)'/';
- $content = preg_replace_callback(
- $pattern,
- function ($matches) use ($prefix) {
- return 'TwigFunction('.$matches[1].', ''.$prefix.'\'.$matches[2].''';
- },
- $content
- );
-
- $pattern = '/TwigTest(('[^']+'),s+'(twig[^']+)'/';
- $content = preg_replace_callback(
- $pattern,
- function ($matches) use ($prefix) {
- return 'TwigTest('.$matches[1].', ''.$prefix.'\'.$matches[2].''';
- },
- $content
- );
- }
-
- if (stristr($filePath, 'ForNode.php')) {
- $content = str_replace(
- ' = twig_ensure_traversable',
- ' = '.$prefix.'\twig_ensure_traversable',
- $content
- );
- }
-
- if (stristr($filePath, 'CaptureNode.php')) {
- $content = str_replace(
- '\Twig',
- $prefix.'\Twig',
- $content
- );
- }
-
- if (stristr($filePath, 'IncludeNode.php') || stristr($filePath, 'WithNode.php')) {
- $content = str_replace(
- 'twig_array_merge(',
- $prefix.'\twig_array_merge(',
- $content
- );
- $content = str_replace(
- 'twig_to_array(',
- $prefix.'\twig_to_array(',
- $content
- );
- $content = str_replace(
- 'twig_test_iterable(',
- $prefix.'\twig_test_iterable(',
- $content
- );
- }
-
- if (stristr($filePath, 'InBinary.php')) {
- $content = str_replace(
- 'twig_in_filter(',
- $prefix.'\twig_in_filter(',
- $content
- );
- }
-
- if (stristr($filePath, 'MethodCallExpression.php')) {
- $content = str_replace(
- 'twig_call_macro(',
- $prefix.'\twig_call_macro(',
- $content
- );
- }
-
- if (stristr($filePath, 'ModuleNode.php')) {
- $content = str_replace(
- 'use Twig\',
- 'use '.$prefix.'\Twig\',
- $content
- );
- }
-
- if (stristr($filePath, 'GetAttrExpression.php')) {
- $content = str_replace(
- 'twig_get_attribute',
- $prefix.'\twig_get_attribute',
- $content
- );
- }
-
- return $content;
- },
- ],
-];
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/admin/onboarding.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/admin/onboarding.php
@@ -59,7 +59,7 @@
*/
public function get_item_permissions_check($request)
{
- if (!current_user_can('administrator')) {
+ if (!current_user_can('manage_options')) {
return new PodloveApiErrorForbiddenAccess();
}
@@ -84,7 +84,7 @@
*/
public function update_item_permissions_check($request)
{
- if (!current_user_can('administrator')) {
+ if (!current_user_can('manage_options')) {
return new PodloveApiErrorForbiddenAccess();
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/admin/plus.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/admin/plus.php
@@ -212,7 +212,7 @@
public function get_features_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
+ if (!current_user_can('manage_options')) {
return new PodloveApiErrorForbiddenAccess();
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/chapters.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/chapters.php
@@ -119,13 +119,14 @@
public function get_item_permissions_check($request)
{
- return true;
+ return PodloveApiEpisodeReadAccess::rest_check($request->get_param('id'));
}
public function get_item($request)
{
$id = $request->get_param('id');
$episode = Episode::find_by_id($id);
+ $data = [];
if ($episode) {
$data = array_map(function ($c) {
@@ -143,11 +144,7 @@
public function create_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function create_item($request)
@@ -198,8 +195,8 @@
}
$chapters->setPrinter(new PrinterMp4chaps());
- $episode_data['chapters'] = (string) $chapters;
- $episode->update_attributes($episode_data);
+ $episode->chapters = (string) $chapters;
+ $episode->save();
return new PodloveApiResponseCreateResponse([
'status' => 'ok'
@@ -208,11 +205,7 @@
public function update_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function update_item($request)
@@ -263,8 +256,8 @@
}
$chapters->setPrinter(new PrinterJSON());
- $episode_data['chapters'] = (string) $chapters;
- $episode->update_attributes($episode_data);
+ $episode->chapters = (string) $chapters;
+ $episode->save();
return new PodloveApiResponseOkResponse([
'status' => 'ok'
@@ -273,11 +266,7 @@
public function delete_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function delete_item($request)
@@ -293,8 +282,8 @@
return new PodloveApiErrorNotFound();
}
- $episode_data['chapters'] = '';
- $episode->update_attributes($episode_data);
+ $episode->chapters = '';
+ $episode->save();
return new PodloveApiResponseOkResponse([
'status' => 'ok'
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/episodes.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/episodes.php
@@ -22,7 +22,7 @@
register_rest_route('podlove/v1', 'episodes/(?P<id>[d]+)', [
'methods' => 'GET',
'callback' => __NAMESPACE__.'episodes_api',
- 'permission_callback' => '__return_true',
+ 'permission_callback' => __NAMESPACE__.'get_episode_permission_check',
]);
register_rest_route('podlove/v1', 'episodes/(?P<id>[d]+)', [
@@ -32,6 +32,11 @@
]);
}
+function get_episode_permission_check($request)
+{
+ return PodloveApiEpisodeReadAccess::rest_check($request->get_param('id'));
+}
+
function list_api()
{
$episodes = Episode::find_all_by_time([
@@ -41,6 +46,10 @@
$results = [];
foreach ($episodes as $episode) {
+ if (!PodloveApiEpisodeReadAccess::can_read($episode)) {
+ continue;
+ }
+
array_push($results, [
'id' => $episode->id,
'title' => get_the_title($episode->post_id),
@@ -92,15 +101,7 @@
*/
function update_episode_permission_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new WP_Error(
- 'rest_forbidden',
- esc_html__('sorry, you do not have permissions to use this REST API endpoint'),
- ['status' => 401]
- );
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
function episodes_update_api($request)
@@ -215,7 +216,7 @@
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'build_slug'],
- 'permission_callback' => [$this, 'create_item_permissions_check'],
+ 'permission_callback' => [$this, 'update_item_permissions_check'],
]
]);
@@ -481,7 +482,7 @@
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_item_tags'],
- 'permission_callback' => [$this, 'delete_item_permissions_check'],
+ 'permission_callback' => [$this, 'delete_item_tags_permissions_check'],
]
]);
}
@@ -537,6 +538,10 @@
$results = [];
foreach ($episodes as $episode) {
+ if (!PodloveApiEpisodeReadAccess::can_read($episode)) {
+ continue;
+ }
+
// filter by show slug
if ($show_slug) {
$show = ShowsModelShow::find_one_by_episode_id($episode->id);
@@ -566,26 +571,7 @@
public function get_item_permissions_check($request)
{
- $id = $request->get_param('id');
- $episode = Episode::find_by_id($id);
- if (!$episode) {
- return false;
- }
-
- $post = $episode->post();
- if (!$post) {
- return false;
- }
-
- if ($post->post_status == 'publish' && $post->post_type == 'podcast') {
- return true;
- }
-
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeReadAccess::rest_check($request->get_param('id'));
}
public function get_item($request)
@@ -724,7 +710,8 @@
public function create_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
+ $post_type = get_post_type_object('podcast');
+ if (!$post_type || !current_user_can($post_type->cap->create_posts)) {
return new PodloveApiErrorForbiddenAccess();
}
@@ -821,11 +808,7 @@
public function update_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function update_item($request)
@@ -1115,11 +1098,12 @@
public function delete_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
+ return PodloveApiEpisodeMutationAccess::rest_check_delete($request->get_param('id'));
+ }
- return true;
+ public function delete_item_tags_permissions_check($request)
+ {
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function delete_item($request)
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/episodes/contributions.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/episodes/contributions.php
@@ -91,17 +91,17 @@
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_contribution'],
- 'permission_callback' => [$this, 'get_item_permissions_check'],
+ 'permission_callback' => [$this, 'get_contribution_permissions_check'],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'update_contribution'],
- 'permission_callback' => [$this, 'update_item_permissions_check'],
+ 'permission_callback' => [$this, 'mutate_contribution_permissions_check'],
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_contribution'],
- 'permission_callback' => [$this, 'delete_item_permissions_check'],
+ 'permission_callback' => [$this, 'mutate_contribution_permissions_check'],
]
]);
}
@@ -190,7 +190,17 @@
public function get_item_permissions_check($request)
{
- return true;
+ return PodloveApiEpisodeReadAccess::rest_check($request->get_param('id'));
+ }
+
+ public function get_contribution_permissions_check($request)
+ {
+ $contribution = EpisodeContribution::find_by_id($request->get_param('id'));
+ if (!$contribution) {
+ return new PodloveApiErrorNotFound();
+ }
+
+ return PodloveApiEpisodeReadAccess::rest_check($contribution->episode_id);
}
public function create_item($request)
@@ -218,11 +228,7 @@
public function create_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function update_item($request)
@@ -352,11 +358,7 @@
public function update_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
- }
-
- return true;
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
}
public function delete_item($request)
@@ -405,11 +407,19 @@
public function delete_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
+ return PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param('id'));
+ }
+
+ public function mutate_contribution_permissions_check($request)
+ {
+ $contribution = EpisodeContribution::find_by_id($request->get_param('id'));
+ if (!$contribution) {
+ return new PodloveApiErrorNotFound();
}
- return true;
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($contribution->episode_id);
+
+ return is_wp_error($access) ? new PodloveApiErrorNotFound() : true;
}
private function isContributorDefault($id)
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/episodes/related_episodes.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/episodes/related_episodes.php
@@ -87,17 +87,17 @@
],
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_item'],
- 'permission_callback' => [$this, 'get_items_permissions_check'],
+ 'permission_callback' => [$this, 'get_relation_permissions_check'],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'update_item'],
- 'permission_callback' => [$this, 'update_items_permissions_check'],
+ 'permission_callback' => [$this, 'mutate_relation_permissions_check'],
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_item'],
- 'permission_callback' => [$this, 'delete_items_permissions_check'],
+ 'permission_callback' => [$this, 'mutate_relation_permissions_check'],
]
]);
}
@@ -116,7 +116,7 @@
$episode = Episode::find_by_id($id);
- if (!$episode || ($filter == 'publish' && !$episode->is_published())) {
+ if (!$episode) {
return new PodloveApiErrorNotFoundEpisode($id);
}
@@ -124,33 +124,35 @@
$results = array_map(function ($relation) use ($filter, $episode) {
$related_id = $relation->left_episode_id;
- $get_left_side = true;
- if ($relation->right_episode_id != $episode->id) {
+ if ($relation->left_episode_id == $episode->id) {
$related_id = $relation->right_episode_id;
- $get_left_side = false;
+ } elseif ($relation->right_episode_id != $episode->id) {
+ return null;
}
+
$related_episode = Episode::find_by_id($related_id);
- if ($related_episode) {
- $related_episode_title = $related_episode->title();
- $post = $related_episode->post();
- if (($filter == 'publish' && $related_episode->is_published())
- || ($post && $filter == 'draft' && $post->post_status == 'draft')
- || $filter == 'all') {
- if ($get_left_side) {
- return [
- 'episode_releation_id' => $relation->id,
- 'related_episode_id' => $relation->left_episode_id,
- 'related_episode_title' => $related_episode_title
- ];
- }
- }
+ if (!$related_episode || !PodloveApiEpisodeReadAccess::can_read($related_episode)) {
+ return null;
+ }
+
+ $post = $related_episode->post();
+ if (!$post) {
+ return null;
+ }
+
+ if ($filter == 'publish' && !PodloveApiEpisodeReadAccess::is_public($related_episode)) {
+ return null;
+ }
- return [
- 'episode_releation_id' => $relation->id,
- 'related_episode_id' => $relation->right_episode_id,
- 'related_episode_title' => $related_episode_title
- ];
+ if ($filter == 'draft' && $post->post_status != 'draft') {
+ return null;
}
+
+ return [
+ 'episode_releation_id' => $relation->id,
+ 'related_episode_id' => $related_episode->id,
+ 'related_episode_title' => $related_episode->title()
+ ];
}, $relations);
// Delete the invalid entries
$results = array_filter($results);
@@ -192,7 +194,7 @@
return new PodloveApiErrorNotFoundEpisode($relation->left_episode_id);
}
- if ($isFilter || ($right_episode->is_published() && $left_episode->is_published())) {
+ if ($isFilter || (PodloveApiEpisodeReadAccess::is_public($right_episode) && PodloveApiEpisodeReadAccess::is_public($left_episode))) {
return new PodloveApiResponseOkResponse([
'_version' => 'v2',
'episode_id' => $left_episode->id,
@@ -213,7 +215,32 @@
return new PodloveApiErrorForbiddenAccess();
}
- return true;
+ $id = $request->get_param('id');
+ if (!$id) {
+ return new PodloveApiErrorNotFound();
+ }
+
+ return PodloveApiEpisodeReadAccess::rest_check($id);
+ }
+
+ public function get_relation_permissions_check($request)
+ {
+ $filter = $request->get_param('status');
+ if ($filter && $filter == 'draft' && !current_user_can('edit_posts')) {
+ return new PodloveApiErrorForbiddenAccess();
+ }
+
+ $relation = EpisodeRelation::find_by_id($request->get_param('id'));
+ if (!$relation) {
+ return new PodloveApiErrorNotFound();
+ }
+
+ $left = PodloveApiEpisodeReadAccess::rest_check($relation->left_episode_id);
+ if (is_wp_error($left)) {
+ return $left;
+ }
+
+ return PodloveApiEpisodeReadAccess::rest_check($relation->right_episode_id);
}
public function update_items($request)
@@ -229,26 +256,23 @@
return new PodloveApiErrorNotFoundEpisode($id);
}
+ $related_ids = $this->get_requested_related_ids($request);
+ foreach ($related_ids as $related_id) {
+ if (!PodloveApiEpisodeMutationAccess::resolve($related_id)) {
+ return new PodloveApiErrorNotFoundEpisode($related_id);
+ }
+ }
+
// Delete all old items
$relations = EpisodeRelation::find_all_by_where('left_episode_id = '.$episode->id);
foreach ($relations as $relation) {
$relation->delete();
}
- if (isset($request['related'])) {
- if (is_array($request['related'])) {
- foreach ($request['related'] as $related_id) {
- $error = $this->create_episode_relation($id, $related_id);
- if (is_wp_error($error)) {
- return $error;
- }
- }
- } else {
- $related_id = $request['related'];
- $error = $this->create_episode_relation($id, $related_id);
- if (is_wp_error($error)) {
- return $error;
- }
+ foreach ($related_ids as $related_id) {
+ $error = $this->create_episode_relation($id, $related_id);
+ if (is_wp_error($error)) {
+ return $error;
}
}
@@ -273,12 +297,18 @@
}
if (isset($request['episode_id'])) {
- $episode_id = $request['episode_id'];
+ $episode_id = (int) $request['episode_id'];
+ if (!Episode::find_by_id($episode_id)) {
+ return new PodloveApiErrorNotFoundEpisode($episode_id);
+ }
$relation->left_episode_id = $episode_id;
}
if (isset($request['related_episode_id'])) {
- $related_id = $request['related_episode_id'];
+ $related_id = (int) $request['related_episode_id'];
+ if (!Episode::find_by_id($related_id)) {
+ return new PodloveApiErrorNotFoundEpisode($related_id);
+ }
$relation->right_episode_id = $related_id;
}
@@ -291,8 +321,25 @@
public function update_items_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
+ $episode_id = $request->get_param('id');
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($episode_id);
+ if (is_wp_error($access)) {
+ return $access;
+ }
+
+ $relations = EpisodeRelation::find_all_by_where('left_episode_id = '.(int) $episode_id);
+ foreach ($relations as $relation) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($relation->right_episode_id);
+ if (is_wp_error($access)) {
+ return $access;
+ }
+ }
+
+ foreach ($this->get_requested_related_ids($request) as $related_id) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($related_id);
+ if (is_wp_error($access)) {
+ return $access;
+ }
}
return true;
@@ -300,21 +347,16 @@
public function create_item($request)
{
- if (isset($request['episode_id'])) {
- $episode_id = $request['episode_id'];
- $episode = Episode::find_by_id($episode_id);
- }
-
- if (isset($request['related_episode_id'])) {
- $related_id = $request['related_episode_id'];
- $related_episode = Episode::find_by_id($related_id);
- }
+ $episode_id = $request->get_param('episode_id');
+ $related_id = $request->get_param('related_episode_id');
+ $episode = Episode::find_by_id($episode_id);
+ $related_episode = Episode::find_by_id($related_id);
if (!$episode) {
- return new PodloveApiErrorNotFoundEpisode($episode->id);
+ return new PodloveApiErrorNotFoundEpisode($episode_id);
}
if (!$related_episode) {
- return new PodloveApiErrorNotFoundEpisode($related_episode->id);
+ return new PodloveApiErrorNotFoundEpisode($related_id);
}
$error = $this->create_episode_relation($episode->id, $related_episode->id);
@@ -330,8 +372,11 @@
public function create_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
+ foreach (['episode_id', 'related_episode_id'] as $parameter) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param($parameter));
+ if (is_wp_error($access)) {
+ return $access;
+ }
}
return true;
@@ -385,13 +430,61 @@
public function delete_items_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
- return new PodloveApiErrorForbiddenAccess();
+ $episode_id = $request->get_param('id');
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($episode_id);
+ if (is_wp_error($access)) {
+ return $access;
+ }
+
+ $relations = EpisodeRelation::find_all_by_where('left_episode_id = '.(int) $episode_id);
+ foreach ($relations as $relation) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($relation->right_episode_id);
+ if (is_wp_error($access)) {
+ return $access;
+ }
}
return true;
}
+ public function mutate_relation_permissions_check($request)
+ {
+ $relation = EpisodeRelation::find_by_id($request->get_param('id'));
+ if (!$relation) {
+ return new PodloveApiErrorNotFound();
+ }
+
+ foreach ([$relation->left_episode_id, $relation->right_episode_id] as $episode_id) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($episode_id);
+ if (is_wp_error($access)) {
+ return new PodloveApiErrorNotFound();
+ }
+ }
+
+ foreach (['episode_id', 'related_episode_id'] as $parameter) {
+ if ($request->has_param($parameter)) {
+ $access = PodloveApiEpisodeMutationAccess::rest_check_edit($request->get_param($parameter));
+ if (is_wp_error($access)) {
+ return $access;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private function get_requested_related_ids($request)
+ {
+ if (!$request->has_param('related')) {
+ return [];
+ }
+
+ $related = $request->get_param('related');
+ $related_ids = is_array($related) ? $related : [$related];
+
+ return array_values(array_unique(array_map('intval', $related_ids)));
+ }
+
private function create_episode_relation($id, $related_id)
{
// Don't create duplicates
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/podcast.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/podcast.php
@@ -128,7 +128,7 @@
*/
public function update_item_permissions_check($request)
{
- if (!current_user_can('edit_posts')) {
+ if (!current_user_can('manage_options')) {
return new PodloveApiErrorForbiddenAccess();
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/api/tools.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/api/tools.php
@@ -36,7 +36,7 @@
public function clear_caches_permission_check()
{
- if (!current_user_can('edit_posts')) {
+ if (!current_user_can('manage_options')) {
return new PodloveApiErrorForbiddenAccess();
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/chapters.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/chapters.php
@@ -71,7 +71,8 @@
foreach ($episodes as $episode) {
if ($chapters = $episode->get_chapters('mp4chaps')) {
- $episode->update_attribute('chapters', $chapters);
+ $episode->chapters = $chapters;
+ $episode->save();
}
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/downloads.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/downloads.php
@@ -267,6 +267,12 @@
exit;
}
+ if (!PodloveApiEpisodeReadAccess::can_read($episode)
+ || (!PodloveApiEpisodeReadAccess::is_public($episode) && !PodloveApiEpisodeReadAccess::is_same_origin_request())) {
+ status_header(404);
+ exit;
+ }
+
do_action('podlove_download_file', $media_file);
// build redirect url
--- a/podlove-podcasting-plugin-for-wordpress/includes/extras.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/extras.php
@@ -61,7 +61,8 @@
if ($cover_art = $episode->cover_art()) {
$url = $cover_art->source_url();
PodloveLog::get()->addInfo('Copying cover art '.$url.' from asset to manual for episode '.$episode->id);
- $episode->update_attribute('cover_art', $url);
+ $episode->cover_art = $url;
+ $episode->save();
}
}
--- a/podlove-podcasting-plugin-for-wordpress/includes/images.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/images.php
@@ -3,6 +3,7 @@
use PodloveCacheHttpHeaderValidator;
use PodloveImageCacheGenerationGuard;
use PodloveImageCacheRequest as ImageCacheRequest;
+use PodloveImageCacheSourcePolicy;
use PodloveLog;
use PodloveModelImage;
use SymfonyComponentYamlYaml;
@@ -26,6 +27,10 @@
foreach ($cache_files as $cache_file) {
$cache = Yaml::parse(file_get_contents($cache_file));
+ if (!SourcePolicy::allows_download($cache['source'] ?? '')) {
+ continue;
+ }
+
if (!isset($cache['etag'])) {
$cache['etag'] = null;
}
@@ -49,6 +54,10 @@
function podlove_refetch_cached_image($url, $filename)
{
+ if (!SourcePolicy::allows_download($url)) {
+ return;
+ }
+
(new Image($url, $filename))->redownload_source();
}
@@ -129,6 +138,10 @@
*/
function podlove_resolve_image_cache_file(ImageCacheRequest $request, $allow_generation)
{
+ if (!SourcePolicy::allows_download($request->source_url())) {
+ return new WP_Error('podlove_image_cache_download_forbidden', __('This image source cannot be cached.'));
+ }
+
$image = new Image($request->source_url(), $request->file_name());
if (!$image->source_exists()) {
--- a/podlove-podcasting-plugin-for-wordpress/includes/podlove-web-player-5.php
+++ b/podlove-podcasting-plugin-for-wordpress/includes/podlove-web-player-5.php
@@ -18,7 +18,7 @@
$post_id = (isset($attributes['post_id']) && $attributes['post_id']) ? $attributes['post_id'] : get_the_ID();
$episode = Episode::find_one_by_post_id($post_id);
- if (!$episode) {
+ if (!$episode || !PodloveApiEpisodeReadAccess::can_read($episode)) {
return [];
}
--- a/podlove-podcasting-plugin-for-wordpress/lib/ajax/ajax.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/ajax/ajax.php
@@ -17,11 +17,9 @@
{
$actions = [
'get-new-guid',
- 'validate-url',
'update-asset-position',
'update-feed-position',
'podcast',
- 'hide-teaser',
'banner-hide',
'onboarding-acknowledge',
'get-license-url',
@@ -60,83 +58,70 @@
public function job_create()
{
- if (!current_user_can('administrator')) {
- http_response_code(401);
- exit;
- }
-
- if (!wp_verify_nonce($_REQUEST['nonce'], 'podlove_ajax')) {
- http_response_code(401);
- exit;
- }
+ MutationAccess::authorize('podlove_ajax', 'nonce', 'manage_options');
- $job_name = filter_input(INPUT_POST, 'name');
- $job_args = isset($_REQUEST['args']) && is_array($_REQUEST['args']) ? $_REQUEST['args'] : [];
+ $job_name = isset($_POST['name']) && is_string($_POST['name'])
+ ? sanitize_text_field(wp_unslash($_POST['name']))
+ : '';
+ $job_args = isset($_POST['args']) && is_array($_POST['args']) ? wp_unslash($_POST['args']) : [];
// check class exists
if (!class_exists($job_name)) {
- self::respond_with_json(['error' => 'job "'.$job_name.'" does not exist']);
+ wp_send_json(['error' => 'job "'.$job_name.'" does not exist']);
}
// check that class is a job
if (!isset(class_uses($job_name)['PodloveJobsJobTrait'])) {
- self::respond_with_json(['error' => '"'.$job_name.'" is not a job']);
+ wp_send_json(['error' => '"'.$job_name.'" is not a job']);
}
$job = PodloveJobsCronJobRunner::create_job($job_name, $job_args);
if ($job) {
- self::respond_with_json([
+ wp_send_json([
'job_id' => $job->get_job_id(),
]);
} else {
- self::respond_with_json(['error' => 'A job "'.$job_name.'" is already running']);
+ wp_send_json(['error' => 'A job "'.$job_name.'" is already running']);
}
}
public function job_get()
{
- if (!current_user_can('administrator')) {
- exit;
+ if (!current_user_can('manage_options')) {
+ MutationAccess::send_error('forbidden', __('You are not allowed to view jobs.'), 403);
}
- $job_id = filter_input(INPUT_GET, 'job_id');
+
+ $job_id = self::positive_integer_parameter($_GET, 'job_id');
$job = PodloveModelJob::find_by_id($job_id);
if (!$job) {
- self::respond_with_json(['error' => 'no job with id "'.$job_id.'"']);
+ MutationAccess::send_error('job_not_found', __('The requested job does not exist.'), 404);
}
- self::respond_with_json($job->to_array());
+ wp_send_json($job->to_array());
}
public function job_delete()
{
- if (!current_user_can('administrator')) {
- http_response_code(401);
- exit;
- }
+ MutationAccess::authorize('podlove_ajax', 'nonce', 'manage_options');
- if (!wp_verify_nonce($_REQUEST['nonce'], 'podlove_ajax')) {
- http_response_code(401);
- exit;
- }
-
- $job_id = filter_input(INPUT_GET, 'job_id');
+ $job_id = self::positive_integer_parameter($_POST, 'job_id');
$job = PodloveModelJob::find_by_id($job_id);
if (!$job) {
- self::respond_with_json(['error' => 'no job with id "'.$job_id.'"']);
+ MutationAccess::send_error('job_not_found', __('The requested job does not exist.'), 404);
}
$job->delete();
- self::respond_with_json(['status' => 'ok']);
+ wp_send_json(['status' => 'ok']);
}
public function jobs_get()
{
- if (!current_user_can('administrator')) {
- exit;
+ if (!current_user_can('manage_options')) {
+ MutationAccess::send_error('forbidden', __('You are not allowed to view jobs.'), 403);
}
$jobs = PodloveModelJob::all();
@@ -181,7 +166,7 @@
return $job;
}, $jobs);
- self::respond_with_json($jobs);
+ wp_send_json($jobs);
}
public function admin_news()
@@ -472,26 +457,49 @@
public static function analytics_settings_tiles_update()
{
- if (!current_user_can('podlove_read_analytics')) {
- exit;
- }
+ MutationAccess::authorize(
+ 'podlove_analytics_preferences',
+ 'nonce',
+ 'podlove_read_analytics'
+ );
- $tile_id = $_GET['tile_id'];
- $checked = isset($_GET['checked']) && $_GET['checked'] === 'checked';
+ $tile_id = isset($_POST['tile_id']) && is_string($_POST['tile_id'])
+ ? sanitize_key(wp_unslash($_POST['tile_id']))
+ : '';
- $option = get_option('podlove_analytics_tiles', []);
+ if (!in_array($tile_id, PodloveSettingsAnalytics::tile_ids(), true)) {
+ MutationAccess::send_error('invalid_tile', __('The analytics tile is invalid.'), 400);
+ }
+
+ $checked = self::boolean_post_parameter('checked');
+ $user_id = get_current_user_id();
+ $option = PodloveSettingsAnalytics::tiles_for_user($user_id);
$option[$tile_id] = $checked;
- update_option('podlove_analytics_tiles', $option);
+
+ update_user_meta($user_id, PodloveSettingsAnalytics::TILES_USER_META, $option);
+
+ wp_send_json_success([
+ 'tile_id' => $tile_id,
+ 'checked' => $checked,
+ ]);
}
public static function analytics_settings_avg_update()
{
- if (!current_user_can('podlove_read_analytics')) {
- exit;
- }
+ MutationAccess::authorize(
+ 'podlove_analytics_preferences',
+ 'nonce',
+ 'podlove_read_analytics'
+ );
+
+ $checked = self::boolean_post_parameter('checked');
+ update_user_meta(
+ get_current_user_id(),
+ PodloveSettingsAnalytics::COMPARE_AVG_USER_META,
+ $checked
+ );
- $checked = isset($_GET['checked']) && $_GET['checked'] === 'checked';
- update_option('podlove_analytics_compare_avg', $checked);
+ wp_send_json_success(['checked' => $checked]);
}
public static function analytics_csv_episodes_table()
@@ -840,101 +848,90 @@
public function get_new_guid()
{
- $post_id = $_REQUEST['post_id'];
+ MutationAccess::authorize('podlove_ajax', 'nonce');
+ $post_id = self::positive_integer_parameter($_POST, 'post_id');
$post = get_post($post_id);
- $guid = PodloveCustom_Guid::guid_for_post($post);
- self::respond_with_json(['guid' => $guid]);
- }
-
- public function validate_url()
- {
- if (!current_user_can('administrator')) {
- echo 'No permission';
-
- exit;
+ if (!$post || $post->post_type !== 'podcast') {
+ MutationAccess::send_error('post_not_found', __('The requested podcast episode does not exist.'), 404);
}
- $file_url = $_REQUEST['file_url'];
-
- $r = wp_remote_head($file_url);
-
- $response_code = $r['response']['code'];
- $reachable = $response_code >= 200 && $response_code < 300;
- $content_length = $r['http_response']->get_headers()['content-length'];
+ if (!current_user_can('edit_post', $post_id)) {
+ MutationAccess::send_error('forbidden', __('You are not allowed to edit this episode.'), 403);
+ }
- $validation_cache = get_option('podlove_migration_validation_cache', []);
- $validation_cache[$file_url] = $reachable;
- update_option('podlove_migration_validation_cache', $validation_cache);
+ $guid = PodloveCustom_Guid::guid_for_post($post);
- self::respond_with_json([
- 'file_url' => $file_url,
- 'reachable' => $reachable,
- 'file_size' => $content_length,
- ]);
+ wp_send_json(['guid' => $guid]);
}
public function update_asset_position()
{
- if (!current_user_can('administrator')) {
- echo 'No permission';
+ MutationAccess::authorize('podlove_ajax', 'nonce', 'manage_options');
- exit;
- }
+ $asset_id = self::positive_integer_parameter($_POST, 'asset_id');
+ $position = self::finite_float_post_parameter('position');
- $asset_id = (int) $_REQUEST['asset_id'];
- $position = (float) $_REQUEST['position'];
+ $asset = ModelEpisodeAsset::find_by_id($asset_id);
- ModelEpisodeAsset::find_by_id($asset_id)
- ->update_attributes(['position' => $position])
- ;
+ if (!$asset) {
+ MutationAccess::send_error('asset_not_found', __('The requested episode asset does not exist.'), 404);
+ }
- exit;
+ $asset->position = $position;
+ $asset->save();
+
+ wp_send_json_success([
+ 'id' => $asset->id,
+ 'position' => (float) $asset->position,
+ ]);
}
public function update_feed_position()
{
- if (!current_user_can('administrator')) {
- echo 'No permission';
+ MutationAccess::authorize('podlove_ajax', 'nonce', 'manage_options');
- exit;
- }
+ $feed_id = self::positive_integer_parameter($_POST, 'feed_id');
+ $position = self::finite_float_post_parameter('position');
- $feed_id = (int) $_REQUEST['feed_id'];
- $position = (float) $_REQUEST['position'];
+ $feed = ModelFeed::find_by_id($feed_id);
- ModelFeed::find_by_id($feed_id)
- ->update_attributes(['position' => $position])
- ;
+ if (!$feed) {
+ MutationAccess::send_error('feed_not_found', __('The requested feed does not exist.'), 404);
+ }
- exit;
- }
+ $feed->position = $position;
+ $feed->save();
- public function hide_teaser()
- {
- update_option('_podlove_hide_teaser', true);
+ wp_send_json_success([
+ 'id' => $feed->id,
+ 'position' => (float) $feed->position,
+ ]);
}
public function banner_hide()
{
- if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_onboarding')) {
- http_response_code(401);
- exit;
- }
+ MutationAccess::authorize(
+ 'podlove_onboarding',
+ '_podlove_nonce',
+ 'manage_options'
+ );
Onboarding::set_banner_hide('true');
+ wp_send_json_success();
}
public function onboarding_acknowledge()
{
- if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_onboarding_acknowledge')) {
- http_response_code(403);
- exit;
- }
+ MutationAccess::authorize(
+ 'podlove_onboarding_acknowledge',
+ '_podlove_nonce'
+ );
$user_id = get_current_user_id();
Onboarding::set_acknowledge_option($user_id, true);
+ wp_send_json_success();
}
public function get_license_url()
@@ -952,6 +949,59 @@
self::respond_with_json(PodloveModelLicense::get_license_from_url($_REQUEST['url']));
}
+ private static function positive_integer_parameter(array $source, string $key): int
+ {
+ if (!isset($source[$key]) || !is_scalar($source[$key])) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ $value = filter_var(wp_unslash($source[$key]), FILTER_VALIDATE_INT, [
+ 'options' => ['min_range' => 1],
+ ]);
+
+ if ($value === false) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ return $value;
+ }
+
+ private static function finite_float_post_parameter(string $key): float
+ {
+ if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ $raw_value = wp_unslash($_POST[$key]);
+
+ if (!is_numeric($raw_value)) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ $value = (float) $raw_value;
+
+ if (!is_finite($value)) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ return $value;
+ }
+
+ private static function boolean_post_parameter(string $key): bool
+ {
+ if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ $value = filter_var(wp_unslash($_POST[$key]), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+
+ if ($value === null) {
+ MutationAccess::send_error('invalid_parameter', sprintf(__('The %s parameter is invalid.'), $key), 400);
+ }
+
+ return $value;
+ }
+
private static function analytics_date_condition()
{
$from = filter_input(INPUT_GET, 'date_from');
--- a/podlove-podcasting-plugin-for-wordpress/lib/ajax/mutation_access.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/ajax/mutation_access.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace PodloveAJAX;
+
+class MutationAccess
+{
+ public static function authorize(
+ string $nonce_action,
+ string $nonce_field,
+ ?string $capability = null,
+ array $capability_args = []
+ ): void {
+ if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
+ if (!headers_sent()) {
+ header('Allow: POST');
+ }
+
+ self::send_error('method_not_allowed', __('This action requires a POST request.'), 405);
+ }
+
+ if (!check_ajax_referer($nonce_action, $nonce_field, false)) {
+ self::send_error('invalid_nonce', __('The request could not be verified.'), 403);
+ }
+
+ if ($capability && !current_user_can($capability, ...$capability_args)) {
+ self::send_error('forbidden', __('You are not allowed to perform this action.'), 403);
+ }
+ }
+
+ public static function send_error(string $code, string $message, int $status): void
+ {
+ wp_send_json_error([
+ 'code' => $code,
+ 'message' => $message,
+ ], $status);
+ }
+}
--- a/podlove-podcasting-plugin-for-wordpress/lib/api/episode_mutation_access.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/api/episode_mutation_access.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace PodloveApi;
+
+use PodloveModelEpisode;
+
+class EpisodeMutationAccess
+{
+ /**
+ * Resolve an episode identifier and validate its backing post.
+ *
+ * @param mixed $episode_id
+ *
+ * @return null|array{episode: Episode, post: WP_Post}
+ */
+ public static function resolve($episode_id)
+ {
+ return EpisodeReadAccess::resolve($episode_id);
+ }
+
+ /**
+ * Resolve a WordPress post identifier to a Publisher episode.
+ *
+ * @param mixed $post_id
+ *
+ * @return null|array{episode: Episode, post: WP_Post}
+ */
+ public static function resolve_by_post_id($post_id)
+ {
+ $post_id = (int) $post_id;
+ if ($post_id <= 0) {
+ return null;
+ }
+
+ $episode = Episode::find_one_by_post_id($post_id);
+ if (!$episode || (int) $episode->post_id !== $post_id) {
+ return null;
+ }
+
+ $post = get_post($post_id);
+ if (!$post || $post->post_type !== 'podcast') {
+ return null;
+ }
+
+ return [
+ 'episode' => $episode,
+ 'post' => $post,
+ ];
+ }
+
+ /**
+ * Check REST edit access for an episode identifier.
+ *
+ * @param mixed $episode_id
+ *
+ * @return true|WP_Error
+ */
+ public static function rest_check_edit($episode_id)
+ {
+ return self::rest_check(self::resolve($episode_id), 'edit_post');
+ }
+
+ /**
+ * Check REST edit access for a WordPress post identifier.
+ *
+ * @param mixed $post_id
+ *
+ * @return true|WP_Error
+ */
+ public static function rest_check_edit_by_post_id($post_id)
+ {
+ return self::rest_check(self::resolve_by_post_id($post_id), 'edit_post');
+ }
+
+ /**
+ * Check REST delete access for an episode identifier.
+ *
+ * @param mixed $episode_id
+ *
+ * @return true|WP_Error
+ */
+ public static function rest_check_delete($episode_id)
+ {
+ return self::rest_check(self::resolve($episode_id), 'delete_post');
+ }
+
+ /**
+ * @param null|array{episode: Episode, post: WP_Post} $resolved
+ * @param string $capability
+ *
+ * @return true|WP_Error
+ */
+ private static function rest_check($resolved, $capability)
+ {
+ if (!$resolved) {
+ return new ErrorNotFound();
+ }
+
+ if (!current_user_can($capability, $resolved['post']->ID)) {
+ return new ErrorForbiddenAccess();
+ }
+
+ return true;
+ }
+}
--- a/podlove-podcasting-plugin-for-wordpress/lib/api/episode_read_access.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/api/episode_read_access.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace PodloveApi;
+
+use PodloveModelEpisode;
+
+class EpisodeReadAccess
+{
+ /**
+ * Resolve an episode and validate its backing post.
+ *
+ * @param mixed $episode_id
+ *
+ * @return null|array{episode: Episode, post: WP_Post}
+ */
+ public static function resolve($episode_id)
+ {
+ $episode = Episode::find_by_id((int) $episode_id);
+ if (!$episode) {
+ return null;
+ }
+
+ $post = $episode->post();
+ if (!$post || $post->post_type !== 'podcast') {
+ return null;
+ }
+
+ return [
+ 'episode' => $episode,
+ 'post' => $post,
+ ];
+ }
+
+ public static function is_public(Episode $episode)
+ {
+ $post = $episode->post();
+
+ return $post
+ && $post->post_type === 'podcast'
+ && $post->post_status === 'publish'
+ && empty($post->post_password);
+ }
+
+ public static function can_read(Episode $episode)
+ {
+ $post = $episode->post();
+ if (!$post || $post->post_type !== 'podcast') {
+ return false;
+ }
+
+ if ($post->post_status === 'publish' && empty($post->post_password)) {
+ return true;
+ }
+
+ if (!is_user_logged_in()) {
+ return false;
+ }
+
+ // WordPress' read_post meta capability does not enforce post passwords.
+ if (!empty($post->post_password)) {
+ return current_user_can('edit_post', $post->ID);
+ }
+
+ return current_user_can('read_post', $post->ID);
+ }
+
+ /**
+ * Non-REST preview endpoints use cookie authentication without a REST nonce,
+ * so unpublished data must not be made available through credentialed CORS.
+ */
+ public static function is_same_origin_request()
+ {
+ if (empty($_SERVER['HTTP_ORIGIN'])) {
+ return true;
+ }
+
+ $request_origin = wp_parse_url(wp_unslash($_SERVER['HTTP_ORIGIN']));
+ $site_origin = wp_parse_url(home_url());
+ if (!$request_origin || !$site_origin) {
+ return false;
+ }
+
+ $request_scheme = strtolower($request_origin['scheme'] ?? '');
+ $site_scheme = strtolower($site_origin['scheme'] ?? '');
+ $request_host = strtolower($request_origin['host'] ?? '');
+ $site_host = strtolower($site_origin['host'] ?? '');
+
+ $default_port = static fn ($scheme) => $scheme === 'https' ? 443 : 80;
+ $request_port = (int) ($request_origin['port'] ?? $default_port($request_scheme));
+ $site_port = (int) ($site_origin['port'] ?? $default_port($site_scheme));
+
+ return $request_scheme === $site_scheme
+ && $request_host === $site_host
+ && $request_port === $site_port;
+ }
+
+ /**
+ * Check REST read access for an episode identifier.
+ *
+ * @param mixed $episode_id
+ *
+ * @return true|WP_Error
+ */
+ public static function rest_check($episode_id)
+ {
+ $resolved = self::resolve($episode_id);
+ if (!$resolved) {
+ return new ErrorNotFound();
+ }
+
+ return self::rest_check_episode($resolved['episode']);
+ }
+
+ /**
+ * Check REST read access for an already resolved episode.
+ *
+ * @return true|WP_Error
+ */
+ public static function rest_check_episode(Episode $episode)
+ {
+ if (self::can_read($episode)) {
+ return true;
+ }
+
+ if (!is_user_logged_in()) {
+ return new ErrorNotFound();
+ }
+
+ return new WP_Error(
+ 'rest_forbidden',
+ esc_html__('sorry, you do not have permissions to read this episode', 'podlove-podcasting-plugin-for-wordpress'),
+ ['status' => 403]
+ );
+ }
+}
--- a/podlove-podcasting-plugin-for-wordpress/lib/api/error.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/api/error.php
@@ -18,7 +18,7 @@
if (strlen($message) == 0) {
$message = esc_html__('sorry, you do not have permissions to use this REST API endpoint');
}
- parent::__construct($code, $message, ['status' => 401]);
+ parent::__construct($code, $message, ['status' => rest_authorization_required_code()]);
}
}
--- a/podlove-podcasting-plugin-for-wordpress/lib/custom_guid.php
+++ b/podlove-podcasting-plugin-for-wordpress/lib/custom_guid.php
@@ -38,16 +38,17 @@
public static function meta_box_callback()
{
+ $guid = (string) get_the_guid();
?>
<div>
- <span id="gu