--- a/wp-rss-aggregator/core/modules/renderer.php
+++ b/wp-rss-aggregator/core/modules/renderer.php
@@ -55,6 +55,12 @@
die();
}
+ $nonce = $data['_wpnonce'] ?? '';
+ if ( ! wp_verify_nonce( $nonce, 'wpra_render_display' ) ) {
+ status_header( 403 );
+ echo 'Nonce verification failed.';
+ die();
+ }
// The $data array now contains all persisted shortcode attributes
// from hx-vals, including id, page, sources, limit, exclude, pagination, template.
// Pass the whole $data array to renderArgs.
--- a/wp-rss-aggregator/core/src/Display/LayoutTrait.php
+++ b/wp-rss-aggregator/core/src/Display/LayoutTrait.php
@@ -131,7 +131,7 @@
return sprintf(
'<span class="feed-author">%s</span>',
- rtrim( $this->ds->authorPrefix ) . ' ' . $authorName
+ esc_html( rtrim( $this->ds->authorPrefix ) . ' ' . $authorName )
);
}
@@ -207,10 +207,17 @@
}
$tag = $block ? 'div' : 'span';
+ $prefix = esc_html( $this->ds->sourcePrefix );
+
+ // $srcName is already HTML from renderLink, so it doesn't need escaping again.
+ // If linking is disabled, $srcName is just the source name string, which needs escaping.
+ if ( ! ( $this->ds->linkSource && $links && ! empty( $url ) ) ) {
+ $srcName = esc_html( $srcName );
+ }
return <<<HTML
<{$tag} class="feed-source">
- {$this->ds->sourcePrefix} {$srcName}
+ {$prefix} {$srcName}
</{$tag}>
HTML;
}
--- a/wp-rss-aggregator/core/src/Display/ListLayout.php
+++ b/wp-rss-aggregator/core/src/Display/ListLayout.php
@@ -2,10 +2,10 @@
namespace RebelCodeAggregatorCoreDisplay;
-use RebelCodeAggregatorCoreDisplayDisplayState;
-use RebelCodeAggregatorCoreDisplayLayoutInterface;
-use RebelCodeAggregatorCoreDisplayLayoutTrait;
use RebelCodeAggregatorCoreIrPost;
+use RebelCodeAggregatorCoreDisplayLayoutTrait;
+use RebelCodeAggregatorCoreDisplayLayoutInterface;
+use RebelCodeAggregatorCoreDisplayDisplayState;
class ListLayout implements LayoutInterface {
@@ -33,9 +33,10 @@
$listStart = ( $state->page - 1 ) * $this->ds->numItems + 1;
$listItems = $this->renderItems( $posts, fn ( IrPost $post ) => $this->item( $post ) );
+ $htmlClass = esc_attr( $this->ds->htmlClass );
return <<<HTML
- <div class="wp-rss-aggregator wpra-list-template {$this->ds->htmlClass}">
+ <div class="wp-rss-aggregator wpra-list-template {$htmlClass}">
<{$listType} class="rss-aggregator wpra-item-list {$listClass}" start="{$listStart}">
{$listItems}
</{$listType}>
@@ -44,8 +45,10 @@
}
private function item( IrPost $post ): string {
+ $htmlClass = esc_attr( $this->ds->htmlClass );
+
return <<<HTML
- <li class="wpra-item feed-item {$this->ds->htmlClass}">
+ <li class="wpra-item feed-item {$htmlClass}">
{$this->renderTitle($post)}
<div class="wprss-feed-meta">
--- a/wp-rss-aggregator/core/src/IrPost/IrImage.php
+++ b/wp-rss-aggregator/core/src/IrPost/IrImage.php
@@ -5,6 +5,7 @@
namespace RebelCodeAggregatorCoreIrPost;
use WP_Post;
+use WP_Error;
use RebelCodeAggregatorCoreUtilsSize;
use RebelCodeAggregatorCoreUtilsResult;
use RebelCodeAggregatorCoreUtilsArrays;
@@ -62,128 +63,237 @@
* @return Result<int> The result, containing the ID of the downloaded image if successful.
*/
public function download( int $postId = 0 ): Result {
- if ( ! function_exists( 'media_sideload_image' ) ) {
+ if ( ! function_exists( 'media_handle_sideload' ) ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
- // Image has an ID already, so it should exist in the media library
+ // Already exists by ID.
if ( $this->id !== null ) {
$existing = get_post( $this->id );
if ( $existing instanceof WP_Post ) {
return Result::Ok( $existing->ID );
- } else {
- return Result::Err( "Image #{$this->id} does not exist in the media library." );
}
+ return Result::Err( "Image #{$this->id} does not exist in the media library." );
}
+ // Base64 / Data URI.
if ( strpos( $this->url, 'data:image' ) === 0 ) {
- global $wp_filesystem;
- if ( ! $wp_filesystem ) {
- require_once ABSPATH . 'wp-admin/includes/file.php';
- WP_Filesystem();
- }
+ return $this->download_base64_image( $postId );
+ }
- list($type, $data) = explode( ';', $this->url );
- list(, $data) = explode( ',', $data );
- $binary = base64_decode( $data );
- $hash = hash( 'sha256', $binary );
-
- $existing = get_posts(
- array(
- 'post_type' => 'attachment',
- 'post_status' => 'any',
- 'meta_query' => array(
- array(
- 'key' => 'wprss_source_data_hash',
- 'value' => $hash,
- ),
+ // Already imported by URL.
+ $existing = query_posts(
+ array(
+ 'post_type' => 'attachment',
+ 'post_status' => 'any',
+ 'meta_query' => array(
+ array(
+ 'key' => ImportedMedia::SOURCE_URL,
+ 'value' => $this->url,
),
- )
- );
+ ),
+ )
+ );
- if ( count( $existing ) > 0 ) {
- return Result::Ok( $existing[0]->ID );
- }
+ if ( count( $existing ) > 0 && is_object( $existing[0] ) ) {
+ return Result::Ok( $existing[0]->ID );
+ }
- $tmp_file_path = wp_tempnam( 'wprss-datauri' );
- if ( ! $tmp_file_path ) {
- return Result::Err( 'Could not create temporary file.' );
- }
+ $desc = $postId > 0
+ ? sprintf( '[Aggregator] Downloaded image for imported item #%d', $postId )
+ : 'Imported by WP RSS Aggregator';
+
+ // Fast path: normal media sideload.
+ $id = media_sideload_image( $this->url, $postId, $desc, 'id' );
+ if ( ! is_wp_error( $id ) ) {
+ update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
+ return Result::Ok( (int) $id );
+ }
- if ( ! $wp_filesystem->put_contents( $tmp_file_path, $binary, FS_CHMOD_FILE ) ) {
- @unlink( $tmp_file_path );
- return Result::Err( 'Could not write to temporary file.' );
- }
+ // Robust fallback sideload.
+ $this->url = trim( html_entity_decode( $this->url ) );
+ $id = $this->sideload_image( $this->url, $postId, $desc );
+ if ( ! is_wp_error( $id ) ) {
+ update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
+ return Result::Ok( (int) $id );
+ }
- $mime_type = str_replace( 'data:', '', $type );
- $extension = '.jpg';
- $mime_to_ext = array(
- 'image/jpeg' => '.jpg',
- 'image/png' => '.png',
- 'image/gif' => '.gif',
- 'image/bmp' => '.bmp',
- 'image/webp' => '.webp',
- );
- if ( isset( $mime_to_ext[ $mime_type ] ) ) {
- $extension = $mime_to_ext[ $mime_type ];
- }
+ // Final browser-safe anti-bot fallback.
+ $id = $this->sideload_image_with_remote_get( $this->url, $postId, $desc );
+ if ( ! is_wp_error( $id ) ) {
+ update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
+ return Result::Ok( (int) $id );
+ }
- $filename = 'image-' . uniqid() . $extension;
+ return Result::Err( 'All image download attempts failed.' );
+ }
- $file_array = array(
- 'name' => $filename,
- 'tmp_name' => $tmp_file_path,
- );
-
- $desc = ( $postId > 0 )
- ? sprintf( __( '[Aggregator] Downloaded image for imported item #%d', 'wpra' ), $postId )
- : __( 'Imported by WP RSS Aggregator', 'wpra' );
-
- $id = media_handle_sideload( $file_array, $postId, $desc );
-
- @unlink( $tmp_file_path );
-
- if ( is_wp_error( $id ) ) {
- return Result::Err( $id->get_error_message() );
- } else {
- update_post_meta( $id, 'wprss_source_data_hash', $hash );
- update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
- return Result::Ok( $id );
- }
+ /**
+ * Base64 image download with hash deduplication.
+ */
+ private function download_base64_image( int $postId ): Result {
+ global $wp_filesystem;
+ if ( ! $wp_filesystem ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ WP_Filesystem();
}
- $existing = query_posts(
+ list($type, $data) = explode( ';', $this->url );
+ list(, $data) = explode( ',', $data );
+ $binary = base64_decode( $data );
+ $hash = hash( 'sha256', $binary );
+
+ $existing = get_posts(
array(
- 'post_type' => 'attachment',
+ 'post_type' => 'attachment',
'post_status' => 'any',
- 'meta_query' => array(
+ 'meta_query' => array(
array(
- 'key' => ImportedMedia::SOURCE_URL,
- 'value' => $this->url,
+ 'key' => 'wprss_source_data_hash',
+ 'value' => $hash,
),
),
+ 'fields' => 'ids',
+ 'numberposts' => 1,
)
);
- if ( count( $existing ) > 0 && is_object( $existing[0] ) ) {
+ if ( count( $existing ) > 0 ) {
return Result::Ok( $existing[0]->ID );
- } else {
- $desc = ( $postId > 0 )
- ? sprintf( __( '[Aggregator] Downloaded image for imported item #%d', 'wpra' ), $postId )
- : __( 'Imported by WP RSS Aggregator', 'wpra' );
-
- $result = media_sideload_image( $this->url, $postId, $desc, 'id' );
-
- if ( is_wp_error( $result ) ) {
- return Result::Err( $result->get_error_message() );
- } else {
- $id = (int) $result;
- update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
- return Result::Ok( $id );
- }
}
+
+ $tmp_file = wp_tempnam( 'wprss-datauri' );
+ if ( ! $tmp_file || ! $wp_filesystem->put_contents( $tmp_file, $binary, FS_CHMOD_FILE ) ) {
+ @unlink( $tmp_file );
+ return Result::Err( 'Failed to create temporary file for Base64 image.' );
+ }
+
+ $mime_to_ext = array(
+ 'image/jpeg' => '.jpg',
+ 'image/png' => '.png',
+ 'image/gif' => '.gif',
+ 'image/bmp' => '.bmp',
+ 'image/webp' => '.webp',
+ );
+ $mime_type = str_replace( 'data:', '', $type );
+ $extension = $mime_to_ext[ $mime_type ] ?? '.jpg';
+ $filename = 'image-' . uniqid() . $extension;
+
+ $file_array = array(
+ 'name' => $filename,
+ 'tmp_name' => $tmp_file,
+ );
+ $desc = $postId > 0
+ ? sprintf( '[Aggregator] Downloaded image for imported item #%d', $postId )
+ : 'Imported by WP RSS Aggregator';
+
+ $id = media_handle_sideload( $file_array, $postId, $desc );
+ @unlink( $tmp_file );
+
+ if ( ! is_wp_error( $id ) ) {
+ update_post_meta( $id, 'wprss_source_data_hash', $hash );
+ update_post_meta( $id, ImportedMedia::SOURCE_URL, $this->url );
+ return Result::Ok( $id );
+ }
+
+ return Result::Err( 'Failed to sideload Base64 image.' );
+ }
+
+ /**
+ * Robust fallback sideload: detects MIME type, fixes extensions, handles WebP, GIF, BMP.
+ */
+ private function sideload_image( string $url, int $postId = 0, string $desc = '' ) {
+ $tmp_file = download_url( $url, 15 );
+ if ( is_wp_error( $tmp_file ) ) {
+ return $tmp_file;
+ }
+
+ $finfo = finfo_open( FILEINFO_MIME_TYPE );
+ $mime_type = finfo_file( $finfo, $tmp_file );
+ finfo_close( $finfo );
+
+ $mime_to_ext = array(
+ 'image/jpeg' => '.jpg',
+ 'image/png' => '.png',
+ 'image/gif' => '.gif',
+ 'image/bmp' => '.bmp',
+ 'image/webp' => '.webp',
+ );
+
+ $extension = $mime_to_ext[ $mime_type ] ?? '.jpg';
+ $filename = basename( parse_url( $url, PHP_URL_PATH ) );
+ if ( ! preg_match( '/.(jpe?g|png|gif|bmp|webp)$/i', $filename ) ) {
+ $filename .= $extension;
+ }
+
+ $file_array = array(
+ 'name' => $filename,
+ 'tmp_name' => $tmp_file,
+ );
+ $id = media_handle_sideload( $file_array, $postId, $desc );
+ if ( is_wp_error( $id ) ) {
+ @unlink( $tmp_file );
+ return $id;
+ }
+
+ return $id;
+ }
+
+ private function sideload_image_with_remote_get( string $url, int $postId = 0, string $desc = '' ) {
+ $url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5 );
+ $url = str_replace( '\/', '/', $url );
+
+ $path = parse_url( $url, PHP_URL_PATH );
+ if ( $path && preg_match( '/.(jpgx|pngx|jpegx)$/i', $path ) ) {
+ $url = preg_replace( '/.([a-z]+)x(?|$)/i', '.$1$2', $url );
+ }
+
+ // Extract host for Referer.
+ $parsed = parse_url($url);
+ $referer = $parsed['scheme'] . '://' . $parsed['host'] ?? '';
+
+ $response = wp_remote_get(
+ $url,
+ array(
+ 'timeout' => 20,
+ 'headers' => array(
+ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
+ 'Accept' => 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
+ 'Accept-Language' => 'en-US,en;q=0.9',
+ 'Referer' => $referer,
+ ),
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $code = wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+ if ( $code !== 200 || empty( $body ) ) {
+ return new WP_Error( 'image_blocked', "Image blocked by remote host (HTTP $code)" );
+ }
+
+ $tmp = wp_tempnam( 'wprss-img' );
+ file_put_contents( $tmp, $body );
+
+ $filename = basename( parse_url( $url, PHP_URL_PATH ) );
+ $filename = preg_replace( '/.(jpgx|pngx|jpegx)$/i', '.jpg', $filename );
+ $file_array = array(
+ 'name' => $filename ?: 'image.jpg',
+ 'tmp_name' => $tmp,
+ );
+
+ $id = media_handle_sideload( $file_array, $postId, $desc );
+ if ( is_wp_error( $id ) ) {
+ @unlink( $tmp );
+ return $id;
+ }
+
+ return $id;
}
/** Converts the IR image into an array. */
--- a/wp-rss-aggregator/core/src/Renderer.php
+++ b/wp-rss-aggregator/core/src/Renderer.php
@@ -198,15 +198,15 @@
if ( 'block' === $type && ! empty( $id ) ) {
$preserved_args = array(
'id' => $id,
- 'align' => $args['align'] ?? null,
- 'limit' => $args['limit'] ?? null,
- 'pagination' => $args['pagination'] ?? null,
+ 'align' => isset( $args['align'] ) ? sanitize_text_field( $args['align'] ) : null,
+ 'limit' => isset( $args['limit'] ) ? sanitize_text_field( $args['limit'] ) : null,
+ 'pagination' => isset( $args['pagination'] ) ? sanitize_text_field( $args['pagination'] ) : null,
);
// Filter out null values to keep $args clean
- $args = array_filter($preserved_args, fn($value) => $value !== null);
+ $args = array_filter( $preserved_args, fn( $value ) => $value !== null );
}
- $v4Slug = trim( $args['template'] ?? '' );
+ $v4Slug = sanitize_text_field( $args['template'] ?? '' );
$display = new Display( null );
if ( ! empty( $v4Slug ) ) {
@@ -246,9 +246,9 @@
assert( $display instanceof Display );
// Process exclusions first
- $excludeSrcsRaw = explode( ',', $args['exclude'] ?? '' );
+ $excludeSrcsRaw = explode( ',', sanitize_text_field( $args['exclude'] ?? '' ) );
$excludeSrcsInput = array_filter( array_map( 'trim', $excludeSrcsRaw ), 'is_numeric' );
- if (!empty($excludeSrcsInput)) {
+ if ( ! empty( $excludeSrcsInput ) ) {
$v4IdMapExclude = $this->sources->resolveV4Ids( $excludeSrcsInput )->getOr( array() );
$v4IdsExclude = array_keys( $v4IdMapExclude );
$v5IdsFromV4Exclude = array_values( $v4IdMapExclude );
@@ -262,19 +262,19 @@
} else {
// If 'exclude' is not in $args, keep existing display settings (if any)
// or ensure it's an empty array if not set.
- $display->settings->excludeSrcs = $display->settings->excludeSrcs ?? [];
+ $display->settings->excludeSrcs = $display->settings->excludeSrcs ?? array();
}
// Process sources: if any source-defining attributes are in $args,
// they override any sources set on the loaded $display.
- $sourceArg = $args['source'] ?? '';
- $sourcesArg = $args['sources'] ?? '';
- $feedsArg = $args['feeds'] ?? '';
+ $sourceArg = sanitize_text_field( $args['source'] ?? '' );
+ $sourcesArg = sanitize_text_field( $args['sources'] ?? '' );
+ $feedsArg = sanitize_text_field( $args['feeds'] ?? '' );
if ( ! empty( $sourceArg ) || ! empty( $sourcesArg ) || ! empty( $feedsArg ) ) {
- $display->sources = []; // Reset sources if specified in args
+ $display->sources = array(); // Reset sources if specified in args
- $sourceIdsInput = [];
+ $sourceIdsInput = array();
$sourceExploded = explode( ',', $sourceArg );
$sourcesExploded = explode( ',', $sourcesArg );
@@ -285,7 +285,7 @@
}
}
- $feedSlugsInput = [];
+ $feedSlugsInput = array();
$feedsExploded = explode( ',', $feedsArg );
foreach ( $feedsExploded as $slug ) {
$slug = trim( $slug );
@@ -298,7 +298,7 @@
$sourceIdsInput[] = $src->id;
}
- $display->sources = array_unique($sourceIdsInput);
+ $display->sources = array_unique( $sourceIdsInput );
}
// If no source args, $display->sources remains as loaded (or default empty).
@@ -310,8 +310,8 @@
}
}
- $categories = explode( ',', $args['category'] ?? '' );
- $folders = explode( ',', $args['folders'] ?? '' );
+ $categories = explode( ',', sanitize_text_field( $args['category'] ?? '' ) );
+ $folders = explode( ',', sanitize_text_field( $args['folders'] ?? '' ) );
foreach ( array_merge( $categories, $folders ) as $folderName ) {
$folderName = trim( $folderName );
if ( ! empty( $folderName ) ) {
@@ -319,11 +319,11 @@
}
}
- $className1 = trim( $args['className'] ?? '' );
+ $className1 = sanitize_html_class( ( $args['className'] ?? '' ) );
$className2 = trim( $display->settings->htmlClass ?? '' );
$display->settings->htmlClass = trim( $className1 . ' ' . $className2 );
- $page = max( 1, $args['page'] ?? 1 );
+ $page = max( 1, sanitize_text_field( $args['page'] ?? 1 ) );
$display = apply_filters( 'wpra.renderer.parseArgs', $display, $args );
@@ -476,18 +476,20 @@
// Persist pagination enable/disable status if it was set
// In renderDisplay, $display->settings->enablePagination is modified by shortcode 'pagination'
- if (isset($display->settings->enablePagination)) {
+ if ( isset( $display->settings->enablePagination ) ) {
$shortcode_args['pagination'] = $display->settings->enablePagination ? 'on' : 'off';
}
- // If there's a V4 slug associated with the display, persist it.
- // parseArgs uses 'template' to load a display.
- // If an 'id' is present, 'id' takes precedence for loading, but 'template' might
- // still be used by filters or other logic in parseArgs if present.
- if (!empty($display->v4Slug)) {
- $shortcode_args['template'] = $display->v4Slug;
- }
+ // If there's a V4 slug associated with the display, persist it.
+ // parseArgs uses 'template' to load a display.
+ // If an 'id' is present, 'id' takes precedence for loading, but 'template' might
+ // still be used by filters or other logic in parseArgs if present.
+ if ( ! empty( $display->v4Slug ) ) {
+ $shortcode_args['template'] = $display->v4Slug;
+ }
+ // Add nonce to the shortcode arguments
+ $shortcode_args['_wpnonce'] = wp_create_nonce( 'wpra_render_display' );
$vals_data = array(
'action' => 'wpra.render.display',
// The 'data' key matches what the AJAX handler in core/modules/renderer.php expects
--- a/wp-rss-aggregator/core/src/V4/V4SourceMigrator.php
+++ b/wp-rss-aggregator/core/src/V4/V4SourceMigrator.php
@@ -172,6 +172,7 @@
$removeFtImage = $meta['wprss_ftp_remove_ft_image'] ?? '0';
$mustHaveFtImage = $meta['wprss_ftp_must_have_ft_image'] ?? '0';
+ $src->settings->postType = $meta['wprss_ftp_post_type'] ?? 'wprss_feed_item';
$src->settings->downloadImages = Bools::normalize( $saveImages );
$src->settings->downloadAllImgSizes = Bools::normalize( $saveAllSizes );
$src->settings->assignFtImage = Bools::normalize( $useFtImage );
--- a/wp-rss-aggregator/wp-rss-aggregator.php
+++ b/wp-rss-aggregator/wp-rss-aggregator.php
@@ -6,7 +6,7 @@
* Plugin Name: WP RSS Aggregator
* Plugin URI: https://wprssaggregator.com
* Description: An RSS importer, aggregator, and auto-blogger plugin for WordPress.
- * Version: 5.0.10
+ * Version: 5.0.11
* Requires at least: 6.2.2
* Requires PHP: 7.4.0
* Author: RebelCode
@@ -38,7 +38,7 @@
}
if ( ! defined( 'WPRA_VERSION' ) ) {
- define( 'WPRA_VERSION', '5.0.10' );
+ define( 'WPRA_VERSION', '5.0.11' );
define( 'WPRA_MIN_PHP_VERSION', '7.4.0' );
define( 'WPRA_MIN_WP_VERSION', '6.2.2' );
define( 'WPRA_FILE', __FILE__ );