--- a/powerpress/powerpress-playlist.php
+++ b/powerpress/powerpress-playlist.php
@@ -54,9 +54,9 @@
{
if( !empty($query_in) )
$query_in .= ',';
- $query_in .= $tt_id;
+ $query_in .= intval($tt_id); // sanitize for sql
}
-
+
if( !empty($query_in) )
{
$terms = $wpdb->get_results("SELECT term_taxonomy_id, term_id, taxonomy FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id IN ($query_in)", ARRAY_A);
--- a/powerpress/powerpress-subscribe.php
+++ b/powerpress/powerpress-subscribe.php
@@ -198,7 +198,8 @@
{
$term_ID = '';
$taxonomy_type = '';
- $Settings = get_option('powerpress_taxonomy_'. intval($taxonomy_term_id), array() );
+ $taxonomy_term_id = intval($taxonomy_term_id); // sanitize for sql
+ $Settings = get_option('powerpress_taxonomy_'. $taxonomy_term_id, array() );
if( !empty($Settings) ) {
global $wpdb;
$term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = {$taxonomy_term_id} LIMIT 1", ARRAY_A);
--- a/powerpress/powerpress.php
+++ b/powerpress/powerpress.php
@@ -3,7 +3,7 @@
Plugin Name: Blubrry PowerPress
Plugin URI: https://blubrry.com/services/powerpress-plugin/
Description: <a href="https://blubrry.com/services/powerpress-plugin/" target="_blank">Blubrry PowerPress</a> is the No. 1 Podcasting plugin for WordPress. Developed by podcasters for podcasters; features include Simple and Advanced modes, multiple audio/video player options, subscribe to podcast tools, podcast SEO features, and more! Fully supports Apple Podcasts (previously iTunes), Google Podcasts, Spotify, and Blubrry Podcasting directories, as well as all podcast applications and clients.
-Version: 11.15.10
+Version: 11.15.11
Author: Blubrry
Author URI: https://blubrry.com/
Requires at least: 3.6
@@ -134,7 +134,7 @@
add_action('init', 'PowerPress_PRT_incidence_response');
// WP_PLUGIN_DIR (REMEMBER TO USE THIS DEFINE IF NEEDED)
-define('POWERPRESS_VERSION', '11.15.10' );
+define('POWERPRESS_VERSION', '11.15.11' );
// Translation support:
if ( !defined('POWERPRESS_ABSPATH') )
@@ -956,6 +956,7 @@
$taxonomy_type = '';
$term_ID = '';
+ $tt_id = intval($tt_id); // sanitize for sql
global $wpdb;
$term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = $tt_id", ARRAY_A);
@@ -1163,7 +1164,7 @@
$Feed = powerpress_merge_empty_feed_settings($CustomFeed, $Feed);
global $wpdb;
- $term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = " . $powerpress_feed['term_taxonomy_id'], ARRAY_A);
+ $term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = " . intval($powerpress_feed['term_taxonomy_id']), ARRAY_A);
$taxonomy_type = $term_info[0]['taxonomy'];
$feed_url = get_term_feed_link($powerpress_feed['term_taxonomy_id'], $taxonomy_type, 'rss2');
}
@@ -1257,7 +1258,8 @@
if( !empty($powerpress_feed['itunes_talent_name']) )
echo "t<itunes:author>" . esc_html($powerpress_feed['itunes_talent_name']) . '</itunes:author>'.PHP_EOL;
- if( !empty($powerpress_feed['explicit']) && $powerpress_feed['explicit'] != 'false' )
+ // itunes:explicit is REQUIRED by Apple on channel level
+ if( !empty($powerpress_feed['explicit']) )
echo "t".'<itunes:explicit>' . $powerpress_feed['explicit'] . '</itunes:explicit>'.PHP_EOL;
if( !empty($Feed['itunes_block']) )
@@ -1947,8 +1949,10 @@
$episode_str = '';
$episode_str .= "tt<podcast:alternateEnclosure ";
- if (!empty($alternate_enclosure['length']) && $alternate_enclosure['length'] > 0) {
- $episode_str .= ' length="' . esc_attr($alternate_enclosure['length']) . '"';
+ // support both 'length' (new) and 'size' (legacy) field names
+ $alt_length = $alternate_enclosure['length'] ?? $alternate_enclosure['size'] ?? 0;
+ if (!empty($alt_length) && $alt_length > 0) {
+ $episode_str .= ' length="' . esc_attr($alt_length) . '"';
}
if (!empty($alternate_enclosure['type'])){
@@ -2140,8 +2144,9 @@
}
echo "tt".'<itunes:episodeType>'. esc_html($EpisodeData['episode_type']) .'</itunes:episodeType>'.PHP_EOL;
- // explicit can have values ['false', 'true', 'false']
- if( !empty($explicit) && $explicit != 'false' ) {
+ // episode explicit only outputs when overriding channel explicit
+ // clean channel+explicit episode | explicit channel + clean episode
+ if( !empty($explicit) && $explicit != $powerpress_feed['explicit'] ) {
echo "tt<itunes:explicit>" . $explicit . '</itunes:explicit>'.PHP_EOL;
}
@@ -5098,12 +5103,13 @@
{
$seconds = 0;
$parts = explode(':', $duration);
+ // phpstan: explode returns strings, type safety
if( count($parts) == 3 )
- $seconds = $parts[2] + ($parts[1]*60) + ($parts[0]*60*60);
+ $seconds = (int)$parts[2] + ((int)$parts[1]*60) + ((int)$parts[0]*60*60);
else if ( count($parts) == 2 )
- $seconds = $parts[1] + ($parts[0]*60);
+ $seconds = (int)$parts[1] + ((int)$parts[0]*60);
else
- $seconds = $parts[0];
+ $seconds = (int)$parts[0];
$hours = 0;
$minutes = 0;
@@ -5147,7 +5153,8 @@
function powerpress_repair_serialize($string)
{
- if( @unserialize($string) )
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ if( @unserialize($string, ['allowed_classes' => false]) )
return $string; // Nothing to repair...
$string = preg_replace_callback('/(s:(d+):"([^"]*)")/',
@@ -5195,7 +5202,8 @@
if ( is_serialized( $meta ) ) // Logic used up but not including WordPress 2.8, new logic doesn't make sure if unserialized failed or not
{
- if ( false !== ( $gm = @unserialize( $meta ) ) )
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ if ( false !== ( $gm = @unserialize( $meta, ['allowed_classes' => false] ) ) )
return $meta;
}
@@ -5263,7 +5271,8 @@
if( $Serialized )
{
- $ExtraData = @unserialize($Serialized);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $ExtraData = @unserialize($Serialized, ['allowed_classes' => false]);
if( $ExtraData && is_array($ExtraData) )
{
foreach( $ExtraData as $key=> $value ) {
@@ -5338,7 +5347,8 @@
{
// Sometimes the stored data gets messed up, we can fix it here:
$podPressMedia = powerpress_repair_serialize($podPressMedia);
- $podPressMedia = @unserialize($podPressMedia);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $podPressMedia = @unserialize($podPressMedia, ['allowed_classes' => false]);
}
// Do it a second time in case it is double serialized
@@ -5346,7 +5356,8 @@
{
// Sometimes the stored data gets messed up, we can fix it here:
$podPressMedia = powerpress_repair_serialize($podPressMedia);
- $podPressMedia = @unserialize($podPressMedia);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $podPressMedia = @unserialize($podPressMedia, ['allowed_classes' => false]);
}
if( is_array($podPressMedia) && isset($podPressMedia[$mediaNum]) && isset($podPressMedia[$mediaNum]['URI']) )
--- a/powerpress/powerpressadmin-editfeed.php
+++ b/powerpress/powerpressadmin-editfeed.php
@@ -98,8 +98,8 @@
}; break;
case 'ttid': {
- $term_taxonomy_id = $type_value;
- $FeedAttribs['term_taxonomy_id'] = $type_value;
+ $term_taxonomy_id = intval($type_value); // sanitize for sql
+ $FeedAttribs['term_taxonomy_id'] = $term_taxonomy_id;
$FeedSettings = powerpress_get_settings('powerpress_taxonomy_'.$term_taxonomy_id);
$FeedSettings = powerpress_default_settings($FeedSettings, 'editfeed_custom');
@@ -156,6 +156,8 @@
}
wp_enqueue_script('powerpress-admin', powerpress_get_root_url() . 'js/admin.js', array(), POWERPRESS_VERSION );
+ $suffix = (defined('WP_DEBUG') && WP_DEBUG) ? '' : '.min';
+ wp_enqueue_script('powerpress-podcast2.0-managers', powerpress_get_root_url() . "js/powerpressadmin-metabox{$suffix}.js", array(), POWERPRESS_VERSION);
?>
<div id="powerpress_settings_page" class="powerpress_tabbed_content">
--- a/powerpress/powerpressadmin-jquery.php
+++ b/powerpress/powerpressadmin-jquery.php
@@ -1314,7 +1314,8 @@
$EnclosureType = trim($EnclosureType);
if ($EnclosureSerialized) {
- $ExtraData = @unserialize($EnclosureSerialized);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $ExtraData = @unserialize($EnclosureSerialized, ['allowed_classes' => false]);
}
$existingLightning = $ExtraData['value_lightning'] ?? [];
--- a/powerpress/powerpressadmin-live-item.php
+++ b/powerpress/powerpressadmin-live-item.php
@@ -162,6 +162,7 @@
foreach($PowerPressTaxonomies as $tt_id => $null) {
$taxonomy_type = '';
$term_ID = '';
+ $tt_id = intval($tt_id); // sanitize for sql
global $wpdb;
$term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = $tt_id", ARRAY_A);
--- a/powerpress/powerpressadmin-metabox.php
+++ b/powerpress/powerpressadmin-metabox.php
@@ -925,15 +925,18 @@
} else {
$chapters_req_url = $PCIChaptersURL;
}
- try {
- $json = file_get_contents($chapters_req_url);
- } catch (Exception $e) {
- $error = true;
- $statusMsg = "We were unable to parse the provided transcript file. Please make sure that it is a .json file.";
- }
+ // phpstan: file_get_contents returns false on failure (doesn't throw), suppress warning instead
+ $json = @file_get_contents($chapters_req_url);
if ($json) {
- $chaptersParsedJson = json_decode($json, true)['chapters'];
+ // phpstan: json_decode returns null on invalid JSON, check before accessing ['chapters']
+ $decoded = json_decode($json, true);
+ if (is_array($decoded) && isset($decoded['chapters'])) {
+ $chaptersParsedJson = $decoded['chapters'];
+ } else {
+ $error = true;
+ $statusMsg = "We were unable to parse the chapters file. Please make sure it contains a valid 'chapters' array.";
+ }
} else {
$error = true;
$statusMsg = "We were unable to parse the provided transcript file. Please make sure that it is publicly accessible and a well-formed .json file.";
--- a/powerpress/powerpressadmin-mt.php
+++ b/powerpress/powerpressadmin-mt.php
@@ -574,11 +574,11 @@
echo '<td '.$class.'><strong>';
if ( current_user_can( 'edit_post', $post_id ) )
{
- ?><a class="row-title" href="<?php echo $edit_link; ?>" title="<?php echo esc_attr(sprintf(__('Edit "%s"', 'powerpress'), $import_data['post_title'])); ?>"><?php echo esc_attr($import_data['post_title']); ?></a><?php
+ ?><a class="row-title" href="<?php echo $edit_link; ?>" title="<?php echo esc_attr(sprintf(__('Edit "%s"', 'powerpress'), $import_data['post_title'])); ?>"><?php echo esc_html($import_data['post_title']); ?></a><?php
}
else
{
- echo $import_data['post_title'];
+ echo esc_html($import_data['post_title']);
}
--- a/powerpress/powerpressadmin-podpress.php
+++ b/powerpress/powerpressadmin-podpress.php
@@ -78,20 +78,21 @@
foreach( $results_data as $null => $row )
{
//$return = $row;
- $podpress_data = @unserialize($row['meta_value']);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $podpress_data = @unserialize($row['meta_value'], ['allowed_classes' => false]);
if( !$podpress_data )
{
$podpress_data_serialized = powerpress_repair_serialize( $row['meta_value'] );
- $podpress_data = @unserialize($podpress_data_serialized);
+ $podpress_data = @unserialize($podpress_data_serialized, ['allowed_classes' => false]);
if( !is_array($podpress_data) && is_string($podpress_data) )
{
- $podpress_data_two = @unserialize($podpress_data);
+ $podpress_data_two = @unserialize($podpress_data, ['allowed_classes' => false]);
if( !is_array($podpress_data_two) )
{
$podpress_data_serialized = powerpress_repair_serialize($podpress_data);
- $podpress_data_two = @unserialize($podpress_data_serialized);
+ $podpress_data_two = @unserialize($podpress_data_serialized, ['allowed_classes' => false]);
}
-
+
if( is_array($podpress_data_two) )
$podpress_data = $podpress_data_two;
}
@@ -99,13 +100,13 @@
else if( is_string($podpress_data) )
{
// May have been double serialized...
- $podpress_unserialized = @unserialize($podpress_data);
+ $podpress_unserialized = @unserialize($podpress_data, ['allowed_classes' => false]);
if( !$podpress_unserialized )
{
$podpress_data_serialized = powerpress_repair_serialize( $podpress_data );
- $podpress_unserialized = @unserialize($podpress_data_serialized);
+ $podpress_unserialized = @unserialize($podpress_data_serialized, ['allowed_classes' => false]);
}
-
+
$podpress_data = $podpress_unserialized;
}
@@ -568,11 +569,11 @@
echo '<td '.$class.'><strong>';
if ( current_user_can( 'edit_post', $post_id ) )
{
- ?><a class="row-title" href="<?php echo $edit_link; ?>" title="<?php echo esc_attr(sprintf(__('Edit "%s"', 'powerpress'), $import_data['post_title'])); ?>"><?php echo $import_data['post_title'] ?></a><?php
+ ?><a class="row-title" href="<?php echo $edit_link; ?>" title="<?php echo esc_attr(sprintf(__('Edit "%s"', 'powerpress'), $import_data['post_title'])); ?>"><?php echo esc_html($import_data['post_title']) ?></a><?php
}
else
{
- echo $import_data['post_title'];
+ echo esc_html($import_data['post_title']);
}
echo '</strong><br />';
--- a/powerpress/powerpressadmin-taxonomyfeeds.php
+++ b/powerpress/powerpressadmin-taxonomyfeeds.php
@@ -132,6 +132,7 @@
foreach($PowerPressTaxonomies as $tt_id => $null) {
$taxonomy_type = '';
$term_ID = '';
+ $tt_id = intval($tt_id); // sanitize for sql
global $wpdb;
$term_info = $wpdb->get_results("SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = $tt_id", ARRAY_A);
--- a/powerpress/powerpressadmin.php
+++ b/powerpress/powerpressadmin.php
@@ -312,20 +312,20 @@
if (file_exists($temp)) {
$ImageData = @getimagesize($temp);
- $rgb = true; // We assume it is RGB
- if (defined('POWERPRESS_IMAGICK') && POWERPRESS_IMAGICK) {
- if ($ImageData[2] == IMAGETYPE_PNG && extension_loaded('imagick')) {
- $image = new Imagick($temp);
- if ($image->getImageColorspace() != imagick::COLORSPACE_RGB) {
- $rgb = false;
+ // phpstan: getimagesize returns false on failure, check before accessing
+ if ($ImageData) {
+ $rgb = true; // We assume it is RGB
+ if (defined('POWERPRESS_IMAGICK') && POWERPRESS_IMAGICK) {
+ if ($ImageData[2] == IMAGETYPE_PNG && extension_loaded('imagick')) {
+ $image = new Imagick($temp);
+ if ($image->getImageColorspace() != imagick::COLORSPACE_RGB) {
+ $rgb = false;
+ }
}
}
- }
-
- if (empty($ImageData['channels']))
- $ImageData['channels'] = 3; // Assume it's ok if we cannot detect it.
- if ($ImageData) {
+ if (empty($ImageData['channels']))
+ $ImageData['channels'] = 3; // Assume it's ok if we cannot detect it.
if ($rgb && ($ImageData[2] == IMAGETYPE_JPEG || $ImageData[2] == IMAGETYPE_PNG) && $ImageData[0] == $ImageData[1] && $ImageData[0] >= 1400 && $ImageData[0] <= 3000 && $ImageData['channels'] == 3) // Just check that it is an image, the correct image type and that the image is square
{
$upload_result = wp_handle_upload($_FILES['itunes_image_file'], array('action' => $_POST['action'], 'test_form' => false));
@@ -809,7 +809,8 @@
}
$valid_recipients[] = [
- 'lightning' => sanitize_email($recipient_data['lightning']),
+ // accepts emails (fountain.fm, alby) or lnurl, sanitize_email can break lnurl format
+ 'lightning' => sanitize_text_field($recipient_data['lightning'] ?? ''),
'pubkey' => sanitize_text_field($recipient_data['pubkey']),
'custom_key' => sanitize_text_field($recipient_data['custom_key']),
'custom_value' => sanitize_text_field($recipient_data['custom_value']),
@@ -817,13 +818,13 @@
'fee' => $is_fee ? 'true' : 'false'
];
}
-
+
if ($split_total > 100)
powerpress_add_error(__('Regular recipient splits exceed 100%. Please adjust split percentages.', 'powerpress'));
if ($fee_split_total > 100)
powerpress_add_error(__('Fee recipient splits exceed 100%. Please adjust fee percentages.', 'powerpress'));
-
+
// replace intaken Feed values with only validated data
$Feed['value_recipients'] = $valid_recipients;
} else {
@@ -921,8 +922,8 @@
$startDate = explode('T', $Feed['live_item']['start_date_time']);
$endDate = explode('T', $Feed['live_item']['end_date_time']);
- $startUnix = strtotime($startDate[0] . ' ' . $startDate[1] . ' ' . htmlspecialchars($Feed['live_item']['timezone']));
- $endUnix = strtotime($endDate[0] . ' ' . $endDate[1] . ' ' . htmlspecialchars($Feed['live_item']['timezone']));
+ $startUnix = strtotime(($startDate[0] ?? '') . ' ' . ($startDate[1] ?? '') . ' ' . htmlspecialchars($Feed['live_item']['timezone']));
+ $endUnix = strtotime(($endDate[0] ?? '') . ' ' . ($endDate[1] ?? '') . ' ' . htmlspecialchars($Feed['live_item']['timezone']));
if ($endUnix <= $startUnix) {
update_option('lit_error', true);
@@ -2285,7 +2286,9 @@
}
// If the field limit is exceeded, WordPress won't send an error so we need to, as this prevents publishing
- if( count($_POST, COUNT_RECURSIVE) > (ini_get('max_input_vars') -100 ) ) {
+ // phpstan: ini_get returns string|false, cast to int and check > 0 before arithmetic
+ $max_input_vars = (int) ini_get('max_input_vars');
+ if( $max_input_vars > 0 && count($_POST, COUNT_RECURSIVE) > ($max_input_vars - 100) ) {
// we want to display the warning message
$error = "PowerPress Warning: you may be exceeding your fields limit, a server setting that limits how many fields your pages can contain. Your current limit is ";
$error .= ini_get('max_input_vars') . " <a href='https://blubrry.com/support/powerpress-documentation/warning-messages-explained/'>Learn more</a>";
@@ -2592,7 +2595,8 @@
}
$valid_recipients[] = [
- 'lightning' => sanitize_email($recipient_data['lightning']),
+ // accepts emails (fountain.fm, alby) or lnurls, sanitize_email can break lnurl format
+ 'lightning' => sanitize_text_field($recipient_data['lightning'] ?? ''),
'pubkey' => sanitize_text_field($recipient_data['pubkey']),
'custom_key' => sanitize_text_field($recipient_data['custom_key']),
'custom_value' => sanitize_text_field($recipient_data['custom_value']),
@@ -2600,13 +2604,13 @@
'fee' => $is_fee ? 'true' : 'false'
];
}
-
+
if ($split_total > 100)
powerpress_add_error(__('Regular recipient splits exceed 100%. Please adjust split percentages.', 'powerpress'));
if ($fee_split_total > 100)
powerpress_add_error(__('Fee recipient splits exceed 100%. Please adjust fee percentages.', 'powerpress'));
-
+
$ToSerialize['value_recipients'] = $valid_recipients;
}
@@ -3079,12 +3083,16 @@
$fname = "temp_chapters.json";
$tempFile = tempnam(sys_get_temp_dir(), $fname);
- $file = fopen($tempFile, 'w');
- fwrite($file, $json);
- fclose($file);
-
- file_put_contents($uploadPath . "/chapters.json", file_get_contents($tempFile));
- unlink($tempFile);
+ // phpstan: tempnam, json_encode, fopen can return false, check before file operations
+ if ($tempFile !== false && $json !== false) {
+ $file = fopen($tempFile, 'w');
+ if ($file !== false) {
+ fwrite($file, $json);
+ fclose($file);
+ file_put_contents($uploadPath . "/chapters.json", file_get_contents($tempFile));
+ unlink($tempFile);
+ }
+ }
$chapterURL = $uploadURL . "/chapters.json";
@@ -5166,6 +5174,27 @@
// =====================
/**
+ * Fetch from API with automatic curl retry on failure
+ *
+ * @param string $url Full URL to fetch
+ * @param string $auth Basic auth string
+ * @param array $post POST data (optional)
+ * @param int $timeout Timeout in seconds
+ *
+ * @return string|false Response body or false on failure
+ */
+function powerpress_fetch_with_retry(string $url, string $auth, array $post = [], int $timeout = 30) {
+ $data = powerpress_remote_fopen($url, $auth, $post, $timeout);
+
+ // retry with curl if primary api failed
+ if (!$data && strpos($url, 'api.blubrry.com') !== false) {
+ $data = powerpress_remote_fopen($url, $auth, $post, $timeout, false, true);
+ }
+
+ return $data;
+}
+
+/**
* Make a blubrry api request
*
* @param string $endpoint_path api endpoint path
@@ -5179,48 +5208,39 @@
*
* @return array|false Decoded JSON response or false on failure
*/
-function powerpress_api_request($endpoint_path, $url_params, $post_data, $settings, $creds, $auth, $api_url_array, $timeout = 1800) {
- $json_data = false;
-
- // build the request url with parameters
+function powerpress_api_request(string $endpoint_path, array $url_params, array $post_data, array $settings, $creds, $auth, array $api_url_array, int $timeout = 1800) {
+ // 1) BUILD REQUEST URL
if (strpos($endpoint_path, '?') !== false) {
- // handle query string separate to avoid '%' being handled as format specifier in vsprintf
- list($path_template, $query_string) = explode('?', $endpoint_path, 2);
- $req_url = vsprintf($path_template, $url_params) . '?' . $query_string;
- } else {
- $req_url = vsprintf($endpoint_path, $url_params);
- }
-
+ // separate query string to avoid '%' being handled as format specifier in vsprintf
+ list($path_template, $query_string) = explode('?', $endpoint_path, 2);
+ $req_url = vsprintf($path_template, $url_params) . '?' . $query_string;
+ } else {
+ $req_url = vsprintf($endpoint_path, $url_params);
+ }
$req_url .= (strpos($req_url, '?') !== false ? '&' : '?') . 'format=json&cache=' . md5(rand(0, 999) . time());
$req_url .= (defined('POWERPRESS_BLUBRRY_API_QSA') ? '&' . POWERPRESS_BLUBRRY_API_QSA : '');
$req_url .= (defined('POWERPRESS_PUBLISH_PROTECTED') ? '&protected=true' : '');
+ // 2) OAUTH PATH: use auth object directly
if ($creds) {
$access_token = powerpress_getAccessToken();
- $results = $auth->api($access_token, $req_url, $post_data, false, $timeout, true, true);
- } else {
- // remove /2 for non-oauth path
- if (strpos($req_url, '/2/') === 0) {
- $req_url = substr($req_url, 2);
- }
- foreach ($api_url_array as $index => $api_url) {
- $full_url = rtrim($api_url, '/') . $req_url;
- $json_data = powerpress_remote_fopen($full_url, $settings['blubrry_auth'], $post_data, $timeout);
+ return $auth->api($access_token, $req_url, $post_data, false, $timeout, true, true);
+ }
- // force cURL for primary api if first attempt failed
- if (!$json_data && $api_url == 'https://api.blubrry.com/') {
- $json_data = powerpress_remote_fopen($full_url, $settings['blubrry_auth'], $post_data, $timeout, false, true);
- }
+ // 3) NON-OAUTH PATH: try each api url with retry
+ if (strpos($req_url, '/2/') === 0) {
+ $req_url = substr($req_url, 2);
+ }
- if ($json_data != false) {
- break;
- }
+ foreach ($api_url_array as $api_url) {
+ $full_url = rtrim($api_url, '/') . $req_url;
+ $json_data = powerpress_fetch_with_retry($full_url, $settings['blubrry_auth'], $post_data, $timeout);
+ if ($json_data) {
+ return powerpress_json_decode($json_data);
}
-
- $results = powerpress_json_decode($json_data);
}
- return $results;
+ return false;
}
/**
@@ -5415,30 +5435,23 @@
*
* @return array|false Array with 'url' and file info if published, false if not
*/
-function powerpress_check_media_published($filename, $program_keyword, $settings, $creds, $auth) {
- $api_url_array = powerpress_get_api_array();
-
- // build request url for media list
+function powerpress_check_media_published(string $filename, string $program_keyword, array $settings, $creds, $auth) {
+ // 1) BUILD API REQUEST URL
$req_url = sprintf('/2/media/%s/index.json?published=true&cache=%s',
urlencode($program_keyword),
md5(rand(0, 999) . time())
);
$req_url .= (defined('POWERPRESS_BLUBRRY_API_QSA') ? '&' . POWERPRESS_BLUBRRY_API_QSA : '');
- $results = false;
-
+ // 2) FETCH PUBLISHED MEDIA LIST
if ($creds) {
$access_token = powerpress_getAccessToken();
$results = $auth->api($access_token, $req_url);
} else {
- foreach ($api_url_array as $index => $api_url) {
+ $results = false;
+ foreach (powerpress_get_api_array() as $api_url) {
$full_url = rtrim($api_url, '/') . $req_url;
- $json_data = powerpress_remote_fopen($full_url, $settings['blubrry_auth']);
-
- if (!$json_data && $api_url == 'https://api.blubrry.com/') {
- $json_data = powerpress_remote_fopen($full_url, $settings['blubrry_auth'], [], 30, false, true);
- }
-
+ $json_data = powerpress_fetch_with_retry($full_url, $settings['blubrry_auth']);
if ($json_data) {
$results = powerpress_json_decode($json_data);
break;
@@ -5446,24 +5459,29 @@
}
}
- if (!is_array($results))
- return false;
+ if (!is_array($results)) return false;
- // search for the filename in the results
+
+ // 3) FIND MATCHING FILENAME IN RESULTS
foreach ($results as $media_item) {
if (!is_array($media_item)) continue;
- // check if this is a published file matching our filename
if (!empty($media_item['published']) &&
!empty($media_item['url']) &&
!empty($media_item['name']) &&
$media_item['name'] === $filename) {
- return [
+ $result = [
'url' => $media_item['url'],
'length' => $media_item['length'] ?? 0,
'published' => true
];
+
+ // ensure podcast_id is saved to post meta
+ if (!empty($media_item['podcast_id'])) {
+ $result['podcast_id'] = $media_item['podcast_id'];
+ }
+ return $result;
}
}
@@ -5493,9 +5511,12 @@
// add post type podcasting feeds if enabled
if (!empty($settings['posttype_podcasting'])) {
$feed_slug_post_types_array = get_option('powerpress_posttype-podcasting');
- foreach ($feed_slug_post_types_array as $feed_slug) {
- if (empty($custom_feeds[$feed_slug])) {
- $custom_feeds[$feed_slug] = $feed_slug;
+ if (is_array($feed_slug_post_types_array)) {
+ // option stores feed_slug => [post_type => title], so iterate keys
+ foreach (array_keys($feed_slug_post_types_array) as $feed_slug) {
+ if (empty($custom_feeds[$feed_slug])) {
+ $custom_feeds[$feed_slug] = $feed_slug;
+ }
}
}
}
@@ -5506,10 +5527,10 @@
$api_url_array = powerpress_get_api_array();
- foreach ($custom_feeds as $feed_slug) {
+ foreach ($custom_feeds as $feed_slug => $feed_title) {
$field = 'enclosure';
if ($feed_slug != 'podcast') {
- $field = '_' . $feed_slug . ':enclosure';
+ $field = "_{$feed_slug}:enclosure";
}
$enclosure_data = get_post_meta($post_id, $field, true);
@@ -5527,7 +5548,8 @@
$enclosure_url = (count($meta_parts) > 0) ? trim($meta_parts[0]) : '';
$enclosure_size = (count($meta_parts) > 1) ? trim($meta_parts[1]) : '';
$enclosure_type = (count($meta_parts) > 2) ? trim($meta_parts[2]) : '';
- $episode_data = (count($meta_parts) > 3) ? unserialize($meta_parts[3]) : false;
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $episode_data = (count($meta_parts) > 3) ? unserialize($meta_parts[3], ['allowed_classes' => false]) : false;
if ($enclosure_type == '') {
$error = __('Blubrry Hosting Error (publish)', 'powerpress') . ': ' . __('Error occurred obtaining enclosure content type.', 'powerpress');
@@ -5570,6 +5592,12 @@
$enclosure_size = $already_published['length'];
}
+ // save podcast_id from already published media
+ if (!empty($already_published['podcast_id'])) {
+ $episode_data['podcast_id'] = $already_published['podcast_id'];
+ $podcast_id = $already_published['podcast_id'];
+ }
+
// save updated enclosure data
$enclosure_data = $enclosure_url . "n" . $enclosure_size . "n" . $enclosure_type . "n" . serialize($episode_data);
update_post_meta($post_id, $field, $enclosure_data);
@@ -5578,12 +5606,10 @@
// get media info (and write tags for mp3) if not already published
if (!$skip_publish) {
// mp3 files: write id3 tags and get info
- if ($is_mp3 && !empty($settings['write_tags'])) {
- $results = powerpress_write_tags($enclosure_url, $post_title, $program_keyword);
- } else {
+ $results = ($is_mp3 && !empty($settings['write_tags']))
+ ? powerpress_write_tags($enclosure_url, $post_title, $program_keyword)
// non-mp3 files or mp3 w/o write_tags: get media info
- $results = powerpress_get_media_info($enclosure_url, $program_keyword);
- }
+ : powerpress_get_media_info($enclosure_url, $program_keyword);
// process media results
if (is_array($results) && !isset($results['error'])) {
@@ -5610,12 +5636,12 @@
// ========================================
if (!$skip_publish && $error == false) {
- $post_vars = array(
- 'episode_art' => $episode_art,
- 'podcast_post_date' => $post_time,
- 'podcast_title' => $post_title,
- 'podcast_subtitle' => isset($episode_data['subtitle']) ? $episode_data['subtitle'] : ''
- );
+ $post_vars = [
+ 'episode_art' => $episode_art,
+ 'podcast_post_date' => $post_time,
+ 'podcast_title' => $post_title,
+ 'podcast_subtitle' => $episode_data['subtitle'] ?? ''
+ ];
// process alternate enclosures
if (!empty($episode_data['alternate_enclosure'])) {
@@ -5634,7 +5660,7 @@
// api request
$results = powerpress_api_request(
'/2/media/%s/%s?publish=true',
- array(urlencode($program_keyword), urlencode($enclosure_url)),
+ [urlencode($program_keyword), urlencode($enclosure_url)],
$post_vars,
$settings,
$creds,
@@ -5720,6 +5746,7 @@
// PUBLISH ALTERNATE ENCLOSURES ONLY
// ========================================
+ $post_vars = []; // init before use
$post_vars['publish_alt_enclosures'] = 1;
$post_vars['alternate_enclosures'] = powerpress_build_alt_enclosure_post_vars(
$episode_data['alternate_enclosure'],
@@ -5759,6 +5786,9 @@
// update alternate enclosures with published urls
if (!empty($results['alternate_enclosures'])) {
foreach ($episode_data['alternate_enclosure'] as $idx => $alternate_enclosure) {
+ // ssrf check on path (consistent with main publish path)
+ if (!SSRFCheck($alternate_enclosure['url'], $feed_slug, false, 'alternate enclosure url basename')) continue;
+
$alt_filename = powerpress_extract_filename($alternate_enclosure['url']);
if (array_key_exists($alt_filename, $results['alternate_enclosures'])) {
$new_alt_url = $results['alternate_enclosures'][$alt_filename];
@@ -5814,7 +5844,8 @@
// ===================================================
// update podcast_id from publish results if available (takes precedence)
- if (!empty($results['podcast_id'])) {
+ // $results may not be set if neither publish path was taken
+ if (isset($results) && !empty($results['podcast_id'])) {
$episode_data['podcast_id'] = $results['podcast_id'];
$podcast_id = $results['podcast_id'];
} else if (empty($podcast_id)) {
@@ -5824,7 +5855,8 @@
if (!empty($postmeta_raw) && is_string($postmeta_raw)) {
$postmeta_parts = explode("n", $postmeta_raw);
if (count($postmeta_parts) > 3) {
- $postmeta_data = @unserialize($postmeta_parts[3]);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $postmeta_data = @unserialize($postmeta_parts[3], ['allowed_classes' => false]);
if (!empty($postmeta_data['podcast_id'])) {
$podcast_id = $postmeta_data['podcast_id'];
}
@@ -6674,6 +6706,8 @@
function powerpress_add_error($error, $debug = [])
{
+ $error = esc_html($error); // escape to prevent xss
+
// if debug context provided, build expandable details
if (!empty($debug)) {
$details = [];
--- a/powerpress/views/episode-box.php
+++ b/powerpress/views/episode-box.php
@@ -127,7 +127,8 @@
$EnclosureType = trim($EnclosureType);
if ($EnclosureSerialized) {
- $ExtraData = @unserialize($EnclosureSerialized);
+ // allowed_classes => false prevents php object injection via crafted serialized data
+ $ExtraData = @unserialize($EnclosureSerialized, ['allowed_classes' => false]);
if ($ExtraData) {
if (isset($ExtraData['duration']))
$iTunesDuration = $ExtraData['duration'];