--- a/wp-recipe-maker/includes/class-wp-recipe-maker.php
+++ b/wp-recipe-maker/includes/class-wp-recipe-maker.php
@@ -31,8 +31,8 @@
* @since 1.0.0
*/
private function define_constants() {
- define( 'WPRM_VERSION', '10.2.4' );
- define( 'WPRM_PREMIUM_VERSION_RECOMMENDED', '10.2.0' );
+ define( 'WPRM_VERSION', '10.3.0' );
+ define( 'WPRM_PREMIUM_VERSION_RECOMMENDED', '10.3.0' );
define( 'WPRM_PREMIUM_VERSION_REQUIRED', '7.0.0' );
define( 'WPRM_POST_TYPE', 'wprm_recipe' );
define( 'WPRM_LIST_POST_TYPE', 'wprm_list' );
--- a/wp-recipe-maker/includes/public/api/class-wprm-api-equipment.php
+++ b/wp-recipe-maker/includes/public/api/class-wprm-api-equipment.php
@@ -68,6 +68,7 @@
'amazon_image_width' => isset( $meta['wprmp_amazon_image_width'] ) ? $meta['wprmp_amazon_image_width'] : '',
'amazon_image_height' => isset( $meta['wprmp_amazon_image_height'] ) ? $meta['wprmp_amazon_image_height'] : '',
'amazon_updated' => isset( $meta['wprmp_amazon_updated'] ) ? $meta['wprmp_amazon_updated'] : '',
+ 'amazon_status' => isset( $meta['wprmp_amazon_status'] ) ? $meta['wprmp_amazon_status'] : '',
'wpupg_custom_link' => isset( $meta['wpupg_custom_link'] ) ? $meta['wpupg_custom_link'] : '',
'wpupg_custom_image' => isset( $meta['wpupg_custom_image'] ) ? $meta['wpupg_custom_image'] : '',
), $object, $meta );
@@ -144,6 +145,10 @@
$amazon_name = $meta['amazon_name'];
update_term_meta( $term->term_id, 'wprmp_amazon_name', $amazon_name );
}
+ if ( isset( $meta['amazon_status'] ) ) {
+ $amazon_status = $meta['amazon_status'];
+ update_term_meta( $term->term_id, 'wprmp_amazon_status', $amazon_status );
+ }
if ( isset( $meta['wpupg_custom_link'] ) ) {
$link = trim( $meta['wpupg_custom_link'] );
update_term_meta( $term->term_id, 'wpupg_custom_link', $link );
--- a/wp-recipe-maker/includes/public/api/class-wprm-api-manage-taxonomies.php
+++ b/wp-recipe-maker/includes/public/api/class-wprm-api-manage-taxonomies.php
@@ -176,6 +176,10 @@
$args['orderby'] = 'meta_value_num';
$args['meta_key'] = 'wprmp_amazon_updated';
break;
+ case 'amazon_status':
+ $args['orderby'] = 'meta_value';
+ $args['meta_key'] = 'wprmp_amazon_status';
+ break;
case 'wpupg_custom_link':
$args['orderby'] = 'meta_value';
$args['meta_key'] = 'wpupg_custom_link';
@@ -277,6 +281,43 @@
);
}
break;
+ case 'amazon_status':
+ if ( 'all' !== $value ) {
+ if ( 'empty' === $value ) {
+ // Filter for items with no ASIN.
+ $args['meta_query'][] = array(
+ 'key' => 'wprmp_amazon_asin',
+ 'compare' => 'NOT EXISTS',
+ );
+ } else if ( 'not_in_stock' === $value ) {
+ // Filter for any status that is NOT "IN_STOCK".
+ // Use custom WHERE clause to exclude items where status is "IN_STOCK" (plain or JSON).
+ $args['wprm_filter_not_in_stock'] = true;
+ // Also ensure ASIN exists (status only makes sense if ASIN is set).
+ $args['meta_query'][] = array(
+ 'key' => 'wprmp_amazon_asin',
+ 'compare' => 'EXISTS',
+ );
+ // Ensure status exists.
+ $args['meta_query'][] = array(
+ 'key' => 'wprmp_amazon_status',
+ 'compare' => 'EXISTS',
+ );
+ } else {
+ // Filter by status type (stored as plain string).
+ $args['meta_query'][] = array(
+ 'key' => 'wprmp_amazon_status',
+ 'compare' => '=',
+ 'value' => $value,
+ );
+ // Also ensure ASIN exists (status only makes sense if ASIN is set).
+ $args['meta_query'][] = array(
+ 'key' => 'wprmp_amazon_asin',
+ 'compare' => 'EXISTS',
+ );
+ }
+ }
+ break;
case 'image_id':
if ( 'all' !== $value ) {
$compare = 'yes' === $value ? 'EXISTS' : 'NOT EXISTS';
@@ -459,6 +500,13 @@
$row->amazon_image_width = get_term_meta( $row->term_id, 'wprmp_amazon_image_width', true );
$row->amazon_image_height = get_term_meta( $row->term_id, 'wprmp_amazon_image_height', true );
$row->amazon_updated = get_term_meta( $row->term_id, 'wprmp_amazon_updated', true );
+ $row->amazon_status = get_term_meta( $row->term_id, 'wprmp_amazon_status', true );
+ // Get non-affiliate product URL for linking.
+ if ( $row->amazon_asin && class_exists( 'WPRMP_Amazon' ) ) {
+ $row->amazon_product_url = WPRMP_Amazon::get_product_url( $row->amazon_asin );
+ } else {
+ $row->amazon_product_url = '';
+ }
$row->product = class_exists( 'WPRMPP_Meta' ) ? WPRMPP_Meta::get_product_from_term_id( $row->term_id ) : false;
break;
case 'suitablefordiet':
@@ -491,12 +539,26 @@
*
* @since 5.0.0
*/
- public static function api_manage_taxonomies_query( $pieces, $taxonomies, $args ) {
+ public static function api_manage_taxonomies_query( $pieces, $taxonomies, $args ) {
+ global $wpdb;
+
$id_search = isset( $args['wprm_search_id'] ) ? $args['wprm_search_id'] : false;
if ( $id_search ) {
$pieces['where'] .= ' AND t.term_id LIKE '%' . esc_sql( $wpdb->esc_like( $id_search ) ) . '%'';
}
+ // Filter for "not_in_stock" - exclude items where status is "IN_STOCK".
+ $filter_not_in_stock = isset( $args['wprm_filter_not_in_stock'] ) ? $args['wprm_filter_not_in_stock'] : false;
+ if ( $filter_not_in_stock ) {
+ // Exclude terms where amazon_status is exactly "IN_STOCK".
+ $pieces['where'] .= " AND NOT EXISTS (
+ SELECT 1 FROM {$wpdb->termmeta} tm
+ WHERE tm.term_id = t.term_id
+ AND tm.meta_key = 'wprmp_amazon_status'
+ AND tm.meta_value = 'IN_STOCK'
+ )";
+ }
+
return $pieces;
}
--- a/wp-recipe-maker/includes/public/class-wprm-assets.php
+++ b/wp-recipe-maker/includes/public/class-wprm-assets.php
@@ -226,6 +226,7 @@
),
'eol' => PHP_EOL,
'latest_recipes' => WPRM_Recipe_Manager::get_latest_recipes( 20, 'id' ),
+ 'latest_posts' => WPRM_Recipe_Manager::get_latest_posts( 20, 'id' ),
'latest_lists' => WPRM_List_Manager::get_latest_lists( 20, 'id' ),
'recipe_templates' => WPRM_Template_Manager::get_templates(),
'addons' => array(
--- a/wp-recipe-maker/includes/public/class-wprm-blocks.php
+++ b/wp-recipe-maker/includes/public/class-wprm-blocks.php
@@ -368,6 +368,10 @@
} else {
$output .= '<style type="text/css">' . WPRM_Assets::get_custom_css( 'recipe' ) . '</style>';
}
+
+ // Calculate and store the position of this roundup item block for counter display.
+ // This ensures the counter shows the correct number in the backend editor.
+ self::calculate_roundup_block_position( $atts );
}
$output .= WPRM_Recipe_Roundup::shortcode( $atts );
@@ -376,6 +380,140 @@
}
/**
+ * Calculate and store the position of a roundup item block in the post content.
+ * This is used to display the correct counter number in the backend editor.
+ *
+ * @since 10.0.0
+ * @param array $atts Block attributes.
+ */
+ private static function calculate_roundup_block_position( $atts ) {
+ // Initialize static variables to track block positions.
+ static $position_cache = null;
+ static $current_position = 0;
+
+ // Get post content if we haven't cached positions yet.
+ if ( null === $position_cache ) {
+ $position_cache = array();
+ $current_position = 0;
+
+ // Try to get post content from various sources.
+ $post_content = '';
+ $post_id = 0;
+
+ // Try to get from global post.
+ if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+ $post_id = $GLOBALS['post']->ID;
+ $post_content = $GLOBALS['post']->post_content;
+ }
+
+ // Try to get from request parameters (REST API).
+ if ( empty( $post_content ) ) {
+ // Check various possible parameter names.
+ $possible_params = array( 'post_id', 'postId', 'context[postId]' );
+ foreach ( $possible_params as $param ) {
+ if ( isset( $_REQUEST[ $param ] ) ) {
+ $post_id = intval( $_REQUEST[ $param ] );
+ break;
+ }
+ }
+
+ // Also check JSON body for REST API requests.
+ if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+ $json_input = file_get_contents( 'php://input' );
+ if ( ! empty( $json_input ) ) {
+ $json_data = json_decode( $json_input, true );
+ if ( $json_data ) {
+ if ( isset( $json_data['post_id'] ) ) {
+ $post_id = intval( $json_data['post_id'] );
+ } elseif ( isset( $json_data['context']['postId'] ) ) {
+ $post_id = intval( $json_data['context']['postId'] );
+ }
+ }
+ }
+ }
+
+ if ( $post_id ) {
+ $post = get_post( $post_id );
+ if ( $post ) {
+ $post_content = $post->post_content;
+ }
+ }
+ }
+
+ // Parse blocks to find all roundup item blocks.
+ if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+ $blocks = parse_blocks( $post_content );
+ if ( ! empty( $blocks ) ) {
+ self::extract_roundup_blocks( $blocks, $position_cache );
+ }
+ }
+ }
+
+ // Create a unique identifier for this block based on its attributes.
+ // Use a combination of id, link, and other distinguishing attributes.
+ $block_key = '';
+ if ( ! empty( $atts['id'] ) ) {
+ $block_key = 'id_' . $atts['id'];
+ } elseif ( ! empty( $atts['link'] ) ) {
+ $block_key = 'link_' . md5( $atts['link'] );
+ } else {
+ // Fallback: use a hash of all attributes.
+ $block_key = 'hash_' . md5( serialize( $atts ) );
+ }
+
+ // If we have cached positions, use them.
+ if ( isset( $position_cache[ $block_key ] ) ) {
+ $GLOBALS['wprm_roundup_block_position'] = $position_cache[ $block_key ];
+ return;
+ }
+
+ // Otherwise, increment position counter (for blocks not found in cache).
+ $current_position++;
+ $GLOBALS['wprm_roundup_block_position'] = $current_position;
+ }
+
+ /**
+ * Recursively extract roundup item blocks from parsed blocks.
+ *
+ * @since 10.0.0
+ * @param array $blocks Parsed block list.
+ * @param array $position_cache Reference to position cache array.
+ */
+ private static function extract_roundup_blocks( $blocks, &$position_cache ) {
+ $position = 0;
+
+ foreach ( $blocks as $block ) {
+ if ( ! is_array( $block ) ) {
+ continue;
+ }
+
+ // Check if this is a roundup item block.
+ if ( isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+ $position++;
+ $attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+
+ // Create a unique identifier for this block.
+ $block_key = '';
+ if ( ! empty( $attrs['id'] ) ) {
+ $block_key = 'id_' . $attrs['id'];
+ } elseif ( ! empty( $attrs['link'] ) ) {
+ $block_key = 'link_' . md5( $attrs['link'] );
+ } else {
+ $block_key = 'hash_' . md5( serialize( $attrs ) );
+ }
+
+ // Store position for this block.
+ $position_cache[ $block_key ] = $position;
+ }
+
+ // Recursively check inner blocks.
+ if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+ self::extract_roundup_blocks( $block['innerBlocks'], $position_cache );
+ }
+ }
+ }
+
+ /**
* Render the recipe snippet block.
*
* @since 6.9.0
--- a/wp-recipe-maker/includes/public/class-wprm-list-shortcode.php
+++ b/wp-recipe-maker/includes/public/class-wprm-list-shortcode.php
@@ -52,6 +52,12 @@
$internal_post_ids = array();
$items = $list->items();
+ // In Gutenberg preview, calculate starting position for roundup items in this list.
+ $roundup_position = 0;
+ if ( WPRM_Context::is_gutenberg_preview() ) {
+ $roundup_position = self::get_roundup_items_before_list( $list_id );
+ }
+
foreach ( $items as $item ) {
if ( 'roundup' === $item['type'] ) {
$data = $item['data'];
@@ -66,14 +72,21 @@
$data['template'] = $list->template();
}
- // Generate shortcode.
+ // In Gutenberg preview, set position for this roundup item.
+ if ( WPRM_Context::is_gutenberg_preview() ) {
+ $roundup_position++;
+ $GLOBALS['wprm_roundup_block_position'] = $roundup_position;
+ }
+
+ // Generate and process shortcode immediately to use the position context.
$shortcode = '[wprm-recipe-roundup-item ';
foreach ( $data as $key => $value ) {
$shortcode .= $key . '="' . self::clean_up_shortcode_attribute( $value ) . '" ';
}
$shortcode = trim( $shortcode ) . ']';
- $output .= $shortcode;
+ // Process shortcode immediately so position context is available.
+ $output .= do_shortcode( $shortcode );
}
if ( 'text' === $item['type'] ) {
@@ -104,8 +117,8 @@
$align_class = ' align' . esc_attr( $atts['align'] );
}
- // Output for list.
- $output = '<div id="wprm-list-' . esc_attr( $atts['id'] ) . '" class="wprm-list' . esc_attr( $align_class ) . '">' . $metadata . do_shortcode( $output ) . '</div>';
+ // Output for list. Note: roundup items are already processed above, so no need to call do_shortcode again.
+ $output = '<div id="wprm-list-' . esc_attr( $atts['id'] ) . '" class="wprm-list' . esc_attr( $align_class ) . '">' . $metadata . $output . '</div>';
}
return $output;
@@ -125,6 +138,150 @@
return $value;
}
+
+ /**
+ * Get count of roundup items that appear before this list in the post content.
+ * Used to calculate correct position numbers for items within the list.
+ *
+ * @since 10.0.0
+ * @param int $list_id ID of the current list.
+ * @return int Count of roundup items before this list.
+ */
+ private static function get_roundup_items_before_list( $list_id ) {
+ static $count_cache = null;
+ static $cached_list_id = null;
+
+ // Cache the count per list to avoid recalculating.
+ if ( null !== $count_cache && $cached_list_id === $list_id ) {
+ return $count_cache;
+ }
+
+ $count = 0;
+
+ // Try to get post content from various sources.
+ $post_content = '';
+ $post_id = 0;
+
+ // Try to get from global post.
+ if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+ $post_id = $GLOBALS['post']->ID;
+ $post_content = $GLOBALS['post']->post_content;
+ }
+
+ // Try to get from request parameters (REST API).
+ if ( empty( $post_content ) ) {
+ // Check various possible parameter names.
+ $possible_params = array( 'post_id', 'postId', 'context[postId]' );
+ foreach ( $possible_params as $param ) {
+ if ( isset( $_REQUEST[ $param ] ) ) {
+ $post_id = intval( $_REQUEST[ $param ] );
+ break;
+ }
+ }
+
+ // Also check JSON body for REST API requests.
+ if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+ $json_input = file_get_contents( 'php://input' );
+ if ( ! empty( $json_input ) ) {
+ $json_data = json_decode( $json_input, true );
+ if ( $json_data ) {
+ if ( isset( $json_data['post_id'] ) ) {
+ $post_id = intval( $json_data['post_id'] );
+ } elseif ( isset( $json_data['context']['postId'] ) ) {
+ $post_id = intval( $json_data['context']['postId'] );
+ }
+ }
+ }
+ }
+
+ if ( $post_id ) {
+ $post = get_post( $post_id );
+ if ( $post ) {
+ $post_content = $post->post_content;
+ }
+ }
+ }
+
+ // Count roundup item blocks that appear before this list block.
+ if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+ $blocks = parse_blocks( $post_content );
+ if ( ! empty( $blocks ) ) {
+ $found_list = false;
+ $count = self::count_roundup_items_before_list( $blocks, $list_id, $found_list );
+ }
+ }
+
+ $count_cache = $count;
+ $cached_list_id = $list_id;
+ return $count;
+ }
+
+ /**
+ * Recursively count roundup items that appear before a specific list block.
+ *
+ * @since 10.0.0
+ * @param array $blocks Parsed block list.
+ * @param int $list_id ID of the list to find.
+ * @param bool $found_list Reference flag indicating if list was found.
+ * @return int Count of roundup items before the list.
+ */
+ private static function count_roundup_items_before_list( $blocks, $list_id, &$found_list ) {
+ $count = 0;
+
+ foreach ( $blocks as $block ) {
+ if ( ! is_array( $block ) ) {
+ continue;
+ }
+
+ // Check if this is the list block we're looking for.
+ if ( isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+ $attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+ if ( isset( $attrs['id'] ) && intval( $attrs['id'] ) === $list_id ) {
+ $found_list = true;
+ // Stop counting once we find the target list.
+ break;
+ }
+ }
+
+ // Count roundup item blocks before finding the target list.
+ if ( ! $found_list && isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+ $count++;
+ }
+
+ // Also count roundup items from list blocks that appear before this one.
+ if ( ! $found_list && isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+ $attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+ $other_list_id = isset( $attrs['id'] ) ? intval( $attrs['id'] ) : 0;
+ if ( $other_list_id && $other_list_id !== $list_id ) {
+ // Count roundup items in this other list.
+ // Only do this if WPRM_List_Manager is available to avoid errors.
+ if ( class_exists( 'WPRM_List_Manager' ) ) {
+ $other_list = WPRM_List_Manager::get_list( $other_list_id );
+ if ( $other_list && method_exists( $other_list, 'items' ) ) {
+ $other_items = $other_list->items();
+ if ( is_array( $other_items ) ) {
+ foreach ( $other_items as $item ) {
+ if ( is_array( $item ) && isset( $item['type'] ) && 'roundup' === $item['type'] ) {
+ $count++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Recursively check inner blocks.
+ if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+ $count += self::count_roundup_items_before_list( $block['innerBlocks'], $list_id, $found_list );
+ if ( $found_list ) {
+ break;
+ }
+ }
+ }
+
+ return $count;
+ }
}
WPRM_List_Shortcode::init();
--- a/wp-recipe-maker/includes/public/class-wprm-metadata-video.php
+++ b/wp-recipe-maker/includes/public/class-wprm-metadata-video.php
@@ -124,6 +124,7 @@
// Only check if there actually is some embed code.
if ( $embed_code ) {
$metadata = $metadata ? $metadata : self::check_for_youtube_embed( $embed_code );
+ $metadata = $metadata ? $metadata : self::check_for_vimeo_embed( $embed_code );
$metadata = $metadata ? $metadata : self::check_for_mediavine_embed( $embed_code, $recipe );
$metadata = $metadata ? $metadata : self::check_for_adthrive_embed( $embed_code );
$metadata = $metadata ? $metadata : self::check_for_wp_youtube_lyte_embed( $embed_code );
@@ -215,6 +216,75 @@
}
/**
+ * Check the embed code for a Vimeo video.
+ *
+ * @since 8.X.X
+ * @param string $embed_code Embed code to check.
+ */
+ private static function check_for_vimeo_embed( $embed_code ) {
+ $metadata = false;
+ $found_vimeo_id = false;
+
+ // Check for Vimeo player URL format: player.vimeo.com/video/ID
+ preg_match( '/player.vimeo.com/video/(d+)/i', $embed_code, $matches );
+ if ( $matches && isset( $matches[1] ) ) {
+ $found_vimeo_id = $matches[1];
+ }
+
+ // Check for standard Vimeo URL format: vimeo.com/ID
+ if ( ! $found_vimeo_id ) {
+ preg_match( '/vimeo.com/(d+)/i', $embed_code, $matches );
+ if ( $matches && isset( $matches[1] ) ) {
+ $found_vimeo_id = $matches[1];
+ }
+ }
+
+ // Try oEmbed with standard Vimeo URL format.
+ if ( $found_vimeo_id ) {
+ $vimeo_url = 'https://vimeo.com/' . $found_vimeo_id;
+ $metadata = self::check_for_oembed( $vimeo_url );
+
+ // If oEmbed didn't return all required fields, try to extract from embed code.
+ if ( $metadata && ( ! $metadata['uploadDate'] || ! $metadata['name'] || ! $metadata['thumbnailUrl'] ) ) {
+ // Try to extract title from iframe title attribute.
+ if ( ! $metadata['name'] ) {
+ preg_match( '/titles*=s*"([^"]+)"/i', $embed_code, $title_match );
+ if ( $title_match && isset( $title_match[1] ) ) {
+ $metadata['name'] = $title_match[1];
+ }
+ }
+
+ // Try Vimeo oEmbed API directly if WordPress oEmbed didn't work.
+ if ( ! $metadata['uploadDate'] || ! $metadata['thumbnailUrl'] ) {
+ $vimeo_oembed_url = 'https://vimeo.com/api/oembed.json?url=' . urlencode( $vimeo_url );
+ $response = wp_remote_get( $vimeo_oembed_url );
+ $body = ! is_wp_error( $response ) && isset( $response['body'] ) ? json_decode( $response['body'] ) : false;
+
+ if ( $body ) {
+ if ( ! $metadata['name'] && isset( $body->title ) ) {
+ $metadata['name'] = $body->title;
+ }
+ if ( ! $metadata['thumbnailUrl'] && isset( $body->thumbnail_url ) ) {
+ $metadata['thumbnailUrl'] = $body->thumbnail_url;
+ }
+ if ( ! $metadata['uploadDate'] && isset( $body->upload_date ) ) {
+ $metadata['uploadDate'] = date( 'c', strtotime( $body->upload_date ) );
+ }
+ }
+ }
+
+ // If still missing required fields, don't return incomplete metadata.
+ // Google requires uploadDate, name, and thumbnailUrl for VideoObject.
+ if ( ! $metadata['uploadDate'] || ! $metadata['name'] || ! $metadata['thumbnailUrl'] ) {
+ $metadata = false;
+ }
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
* Check the embed code for a YouTube video.
*
* @since 8.1.0
@@ -469,10 +539,29 @@
return 'https://www.youtube.com/watch?v=' . $match[2];
}
+ // Check for Vimeo player URL and convert to standard format.
+ preg_match( '/player.vimeo.com/video/(d+)/i', $embed_code, $match );
+ if ( $match && isset( $match[1] ) ) {
+ return 'https://vimeo.com/' . $match[1];
+ }
+
+ // Check for standard Vimeo URL.
+ preg_match( '/vimeo.com/(d+)/i', $embed_code, $match );
+ if ( $match && isset( $match[1] ) ) {
+ return 'https://vimeo.com/' . $match[1];
+ }
+
// Check for src="" in the embed code.
preg_match( '/srcs*=s*"([^"]+)"/im', $embed_code, $match );
if ( $match && isset( $match[1] ) ) {
- return $match[1];
+ $url = $match[1];
+
+ // Convert Vimeo player URLs to standard format.
+ if ( preg_match( '/player.vimeo.com/video/(d+)/i', $url, $vimeo_match ) ) {
+ return 'https://vimeo.com/' . $vimeo_match[1];
+ }
+
+ return $url;
}
return false;
--- a/wp-recipe-maker/includes/public/class-wprm-metadata.php
+++ b/wp-recipe-maker/includes/public/class-wprm-metadata.php
@@ -38,7 +38,6 @@
add_filter( 'wpseo_schema_graph_pieces', array( __CLASS__, 'wpseo_schema_graph_pieces' ), 1, 2 );
add_filter( 'wpseo_schema_graph', array( __CLASS__, 'wpseo_schema_graph' ), 99, 2 );
- add_filter( 'wpseo_opengraph_type', array( __CLASS__, 'wpseo_opengraph_type' ), 99 );
}
/**
@@ -229,24 +228,6 @@
}
/**
- * Yoast SEO filter open graph type
- *
- * @since 9.3.0
- * @param string $type The type.
- */
- public static function wpseo_opengraph_type( $type ) {
- if ( self::use_yoast_seo_integration() ) {
- $recipe_ids_to_output_metadata_for = self::get_recipe_ids_to_output();
-
- if ( $recipe_ids_to_output_metadata_for ) {
- $type = 'recipe';
- }
- }
-
- return $type;
- }
-
- /**
* Register image sizes for the recipe metadata.
*
* @since 1.25.0
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-manager.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-manager.php
@@ -46,6 +46,8 @@
add_action( 'wp_ajax_wprm_get_recipe', array( __CLASS__, 'ajax_get_recipe' ) );
add_action( 'wp_ajax_wprm_search_recipes', array( __CLASS__, 'ajax_search_recipes' ) );
add_action( 'wp_ajax_wprm_search_posts', array( __CLASS__, 'ajax_search_posts' ) );
+ add_action( 'wp_ajax_wprm_create_post_for_recipe', array( __CLASS__, 'ajax_create_post_for_recipe' ) );
+ add_action( 'wp_ajax_wprm_add_recipe_to_post', array( __CLASS__, 'ajax_add_recipe_to_post' ) );
add_action( 'wp_footer', array( __CLASS__, 'recipe_data_in_footer' ) );
}
@@ -82,6 +84,11 @@
$posts = $query->posts;
foreach ( $posts as $post ) {
+ // Only include recipes the user can read/edit to respect WordPress access controls.
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ continue;
+ }
+
$recipes[ $post->ID ] = array(
'name' => $post->post_title,
);
@@ -124,6 +131,11 @@
$posts = $query->posts;
foreach ( $posts as $post ) {
+ // Only include recipes the user can read/edit to respect WordPress access controls.
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ continue;
+ }
+
// Special case.
if ( 'manage' === $display ) {
$recipe = self::get_recipe( $post );
@@ -154,12 +166,92 @@
}
/**
+ * Get latest posts.
+ *
+ * @since 9.0.0
+ * @param int $limit Number of posts to get.
+ * @param string $display Display format ('name' or 'id').
+ * @return array Array of posts with 'id' and 'text' keys.
+ */
+ public static function get_latest_posts( $limit = 10, $display = 'name' ) {
+ $posts = array();
+
+ // Get allowed post types (exclude recipes and attachments).
+ $ignore_post_types = array(
+ WPRM_POST_TYPE,
+ 'attachment',
+ );
+ $public_post_types = get_post_types( array( 'public' => true ), 'names' );
+ $allowed_post_types = array_diff( $public_post_types, $ignore_post_types );
+
+ // If no allowed post types, return empty array.
+ if ( empty( $allowed_post_types ) ) {
+ return array();
+ }
+
+ $args = array(
+ 'post_type' => $allowed_post_types,
+ 'post_status' => 'any',
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'posts_per_page' => $limit,
+ 'offset' => 0,
+ 'suppress_filters' => true,
+ 'lang' => '',
+ );
+
+ $query = new WP_Query( $args );
+
+ if ( $query->have_posts() ) {
+ $query_posts = $query->posts;
+ $post_type_cache = array(); // Cache post type objects.
+
+ foreach ( $query_posts as $post ) {
+ // Only include posts the user can read/edit to respect WordPress access controls.
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ continue;
+ }
+
+ // Get post type name (cached).
+ if ( ! isset( $post_type_cache[ $post->post_type ] ) ) {
+ $post_type_object = get_post_type_object( $post->post_type );
+ $post_type_cache[ $post->post_type ] = $post_type_object ? $post_type_object->labels->singular_name : $post->post_type;
+ }
+ $post_type_label = $post_type_cache[ $post->post_type ];
+
+ switch ( $display ) {
+ case 'id':
+ $text = $post_type_label . ' - ' . $post->ID . ' - ' . $post->post_title;
+ break;
+ default:
+ $text = $post->post_title;
+ }
+
+ $posts[] = array(
+ 'id' => $post->ID,
+ 'text' => $text,
+ );
+ }
+ }
+
+ return $posts;
+ }
+
+ /**
* Search for recipes by keyword.
*
* @since 1.8.0
*/
public static function ajax_search_recipes() {
if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+ // Require edit_posts capability to prevent unauthorized access.
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; // Input var okay.
$recipes = array();
@@ -197,6 +289,11 @@
}
foreach ( $posts as $post ) {
+ // Only include recipes the user can read/edit to respect WordPress access controls.
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
+ continue;
+ }
+
$recipes[] = array(
'id' => $post->ID,
'text' => $post->post_title,
@@ -224,13 +321,39 @@
*/
public static function ajax_search_posts() {
if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+ // Require edit_posts capability to prevent unauthorized access.
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; // Input var okay.
$found_posts = array();
$found_posts_with_id = array();
+ $seen_post_ids = array(); // Track post IDs to prevent duplicates.
+
+ // Get allowed post types (exclude recipes and attachments).
+ $ignore_post_types = array(
+ WPRM_POST_TYPE,
+ 'attachment',
+ );
+ $public_post_types = get_post_types( array( 'public' => true ), 'names' );
+ $allowed_post_types = array_diff( $public_post_types, $ignore_post_types );
+
+ // If no allowed post types, return empty results.
+ if ( empty( $allowed_post_types ) ) {
+ wp_send_json_success( array(
+ 'posts' => array(),
+ 'posts_with_id' => array(),
+ ) );
+ wp_die();
+ }
$args = array(
- 'post_type' => 'any',
+ 'post_type' => $allowed_post_types,
'post_status' => 'any',
'posts_per_page' => 100,
's' => $search,
@@ -248,7 +371,7 @@
if ( $id > 0 ) {
$args = array(
- 'post_type' => 'any',
+ 'post_type' => $allowed_post_types,
'post_status' => 'any',
'posts_per_page' => 100,
'post__in' => array( $id ),
@@ -256,19 +379,34 @@
$query = new WP_Query( $args );
- $posts = array_merge( $query->posts, $posts );
+ // Merge and deduplicate by post ID.
+ $id_posts = array();
+ foreach ( $query->posts as $id_post ) {
+ $id_posts[ $id_post->ID ] = $id_post;
+ }
+ foreach ( $posts as $post ) {
+ if ( ! isset( $id_posts[ $post->ID ] ) ) {
+ $id_posts[ $post->ID ] = $post;
+ }
+ }
+ $posts = array_values( $id_posts );
}
}
foreach ( $posts as $post ) {
- $ignore_post_types = array(
- WPRM_POST_TYPE,
- 'attachment',
- );
- if ( in_array( $post->post_type, $ignore_post_types ) ) {
+ // Skip if we've already processed this post ID.
+ if ( isset( $seen_post_ids[ $post->ID ] ) ) {
+ continue;
+ }
+
+ // Only include posts the user can read/edit to respect WordPress access controls.
+ if ( ! current_user_can( 'read_post', $post->ID ) ) {
continue;
}
+ // Mark this post ID as seen.
+ $seen_post_ids[ $post->ID ] = true;
+
$found_posts[] = array(
'id' => $post->ID,
'text' => $post->post_title,
@@ -300,8 +438,26 @@
*/
public static function ajax_get_recipe() {
if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+ // Require edit_posts capability to prevent unauthorized access.
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
$recipe_id = isset( $_POST['recipe_id'] ) ? intval( $_POST['recipe_id'] ) : 0; // Input var okay.
+ if ( $recipe_id > 0 ) {
+ // Only return recipe data if the user can read/edit the recipe.
+ if ( ! current_user_can( 'read_post', $recipe_id ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to access this recipe.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+ }
+
$recipe = self::get_recipe( $recipe_id );
$recipe_data = $recipe ? $recipe->get_data() : array();
@@ -311,6 +467,269 @@
}
wp_die();
+ }
+
+ /**
+ * Get user's editor preference (block or classic).
+ *
+ * @since 1.0.0
+ * @return bool True if user prefers block editor, false for classic editor.
+ */
+ private static function get_user_editor_preference() {
+ $use_block_editor = true;
+
+ if ( class_exists( 'Classic_Editor' ) ) {
+ $user_id = get_current_user_id();
+ $user_editor = get_user_meta( $user_id, 'editor', true );
+ $use_block_editor = ( 'classic' !== $user_editor );
+ }
+
+ return $use_block_editor;
+ }
+
+ /**
+ * Determine if a post uses block editor.
+ *
+ * @since 1.0.0
+ * @param WP_Post|int $post Post object or post ID.
+ * @return bool True if post uses block editor, false otherwise.
+ */
+ private static function post_uses_block_editor( $post ) {
+ if ( function_exists( 'use_block_editor_for_post' ) ) {
+ return use_block_editor_for_post( $post );
+ }
+
+ // Fallback: check meta if Classic Editor plugin is active
+ if ( class_exists( 'Classic_Editor' ) ) {
+ $post_id = is_object( $post ) ? $post->ID : absint( $post );
+ $classic_remember = get_post_meta( $post_id, 'classic-editor-remember', true );
+ return ( 'classic-editor' !== $classic_remember );
+ }
+
+ // No Classic Editor plugin, assume block editor
+ return true;
+ }
+
+ /**
+ * Set editor preference meta for a post.
+ *
+ * @since 1.0.0
+ * @param int $post_id Post ID.
+ * @param bool $use_block_editor Whether to use block editor.
+ * @return string Edit link with appropriate query args.
+ */
+ private static function set_post_editor_preference( $post_id, $use_block_editor ) {
+ $edit_link = admin_url( add_query_arg( array(
+ 'post' => $post_id,
+ 'action' => 'edit',
+ ), 'post.php' ) );
+
+ // Remove any existing editor preference
+ delete_post_meta( $post_id, 'classic-editor-remember' );
+ delete_post_meta( $post_id, '_wp_use_block_editor_for_post' );
+
+ if ( class_exists( 'Classic_Editor' ) ) {
+ if ( ! $use_block_editor ) {
+ // User prefers classic editor
+ update_post_meta( $post_id, 'classic-editor-remember', 'classic-editor' );
+ } else {
+ // User prefers block editor (or no preference, default to block)
+ update_post_meta( $post_id, '_wp_use_block_editor_for_post', true );
+ $edit_link = add_query_arg( 'classic-editor__forget', '', $edit_link );
+ }
+ } else {
+ // Classic Editor plugin not active, ensure block editor
+ update_post_meta( $post_id, '_wp_use_block_editor_for_post', true );
+ }
+
+ return $edit_link;
+ }
+
+ /**
+ * Format recipe content for insertion into post.
+ *
+ * @since 1.0.0
+ * @param int $recipe_id Recipe ID.
+ * @param bool $use_block_editor Whether to use block format.
+ * @return string Formatted recipe content.
+ */
+ private static function format_recipe_content( $recipe_id, $use_block_editor ) {
+ if ( $use_block_editor ) {
+ // Use block format for block editor
+ return '<!-- wp:wp-recipe-maker/recipe {"id":' . absint( $recipe_id ) . '} /-->';
+ } else {
+ // Use shortcode for classic editor
+ return '[wprm-recipe id="' . absint( $recipe_id ) . '"]';
+ }
+ }
+
+ /**
+ * Create a new post for a recipe.
+ *
+ * @since 1.0.0
+ */
+ public static function ajax_create_post_for_recipe() {
+ if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+ // Require edit_posts capability to prevent unauthorized access.
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
+ $recipe_id = isset( $_POST['recipe_id'] ) ? absint( $_POST['recipe_id'] ) : 0; // Input var okay.
+
+ if ( $recipe_id > 0 ) {
+ // Only create post if the user can read/edit the recipe.
+ if ( ! current_user_can( 'read_post', $recipe_id ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to access this recipe.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
+ $recipe = self::get_recipe( $recipe_id );
+
+ if ( $recipe ) {
+ // Validate post type capabilities
+ $post_type = 'post';
+ $post_type_object = get_post_type_object( $post_type );
+
+ if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->create_posts ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to create posts.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
+ // Determine if post will use block editor
+ $use_block_editor = self::get_user_editor_preference();
+
+ // Format content based on editor type
+ $content = self::format_recipe_content( $recipe_id, $use_block_editor );
+
+ $post = array(
+ 'post_type' => $post_type,
+ 'post_status' => 'draft',
+ 'post_title' => sanitize_text_field( $recipe->name() ),
+ 'post_content' => $content,
+ );
+
+ $post_id = wp_insert_post( $post );
+
+ if ( is_wp_error( $post_id ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'Failed to create post: ', 'wp-recipe-maker' ) . $post_id->get_error_message(),
+ ) );
+ wp_die();
+ }
+
+ if ( $post_id ) {
+ // Set editor preference and get edit link
+ $edit_link = self::set_post_editor_preference( $post_id, $use_block_editor );
+
+ wp_send_json_success( array(
+ 'editLink' => $edit_link,
+ ) );
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Failed to create post.', 'wp-recipe-maker' ),
+ ) );
+ }
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Recipe not found.', 'wp-recipe-maker' ),
+ ) );
+ }
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Invalid recipe ID.', 'wp-recipe-maker' ),
+ ) );
+ }
+ }
+
+ wp_die();
+ }
+
+ /**
+ * Add recipe shortcode to an existing post.
+ *
+ * @since 1.0.0
+ */
+ public static function ajax_add_recipe_to_post() {
+ if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+ // Require edit_posts capability to prevent unauthorized access.
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
+ $recipe_id = isset( $_POST['recipe_id'] ) ? absint( $_POST['recipe_id'] ) : 0; // Input var okay.
+ $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; // Input var okay.
+
+ if ( $recipe_id > 0 && $post_id > 0 ) {
+ // Only add recipe if the user can read/edit both the recipe and the post.
+ if ( ! current_user_can( 'read_post', $recipe_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+ ) );
+ wp_die();
+ }
+
+ $recipe = self::get_recipe( $recipe_id );
+ $post = get_post( $post_id );
+
+ if ( $recipe && $post ) {
+ // Determine if post uses block editor (consistent with new post creation)
+ $use_block_editor = self::post_uses_block_editor( $post );
+
+ // Format content based on editor type
+ $recipe_block = "nn" . self::format_recipe_content( $recipe_id, $use_block_editor );
+
+ // Append recipe to existing content
+ $content = $post->post_content . $recipe_block;
+
+ $updated = wp_update_post( array(
+ 'ID' => $post_id,
+ 'post_content' => $content,
+ ) );
+
+ if ( is_wp_error( $updated ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'Failed to update post: ', 'wp-recipe-maker' ) . $updated->get_error_message(),
+ ) );
+ wp_die();
+ }
+
+ if ( $updated ) {
+ // Don't change editor preference for existing posts - use get_edit_post_link
+ // which respects the post's current editor preference
+ $edit_link = get_edit_post_link( $post_id, '' );
+
+ wp_send_json_success( array(
+ 'editLink' => $edit_link,
+ ) );
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Failed to update post.', 'wp-recipe-maker' ),
+ ) );
+ }
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Recipe or post not found.', 'wp-recipe-maker' ),
+ ) );
+ }
+ } else {
+ wp_send_json_error( array(
+ 'message' => __( 'Invalid recipe ID or post ID.', 'wp-recipe-maker' ),
+ ) );
+ }
+ }
+
+ wp_die();
}
/**
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-sanitizer.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-sanitizer.php
@@ -54,6 +54,7 @@
// HTML fields.
if ( isset( $recipe['summary'] ) ) { $sanitized_recipe['summary'] = self::sanitize_html( $recipe['summary'] ); }
+ if ( isset( $recipe['author_bio'] ) ) { $sanitized_recipe['author_bio'] = self::sanitize_html( $recipe['author_bio'] ); }
if ( isset( $recipe['notes'] ) ) { $sanitized_recipe['notes'] = self::sanitize_notes( $recipe['notes'] ); }
// Number fields.
@@ -206,6 +207,31 @@
'notes' => isset( $ingredient['notes'] ) ? self::sanitize_html( $ingredient['notes'] ) : '',
);
+ // Ingredient splits (using percentages).
+ if ( isset( $ingredient['splits'] ) && is_array( $ingredient['splits'] ) ) {
+ $sanitized_splits = array();
+ $total_percentage = 0;
+
+ foreach ( $ingredient['splits'] as $split ) {
+ if ( isset( $split['id'] ) && isset( $split['percentage'] ) ) {
+ $percentage = floatval( $split['percentage'] );
+ // Validate percentage is between 0 and 100
+ if ( $percentage >= 0 && $percentage <= 100 ) {
+ $sanitized_split = array(
+ 'id' => intval( $split['id'] ),
+ 'percentage' => $percentage,
+ );
+ $sanitized_splits[] = $sanitized_split;
+ $total_percentage += $percentage;
+ }
+ }
+ }
+ // Only add splits array if we have at least 2 splits and percentages sum to approximately 100%
+ if ( count( $sanitized_splits ) >= 2 && abs( $total_percentage - 100 ) < 0.01 ) {
+ $sanitized_ingredient['splits'] = $sanitized_splits;
+ }
+ }
+
// Product amount.
if ( isset( $ingredient['product_amount'] ) && $ingredient['product_amount'] !== '' ) {
$product_amount = floatval( $ingredient['product_amount'] );
@@ -307,12 +333,31 @@
if ( isset( $instruction_group['instructions'] ) ) {
foreach ( $instruction_group['instructions'] as $instruction ) {
+ // Sanitize ingredients - handle both regular UIDs (integers) and splits (strings like "1:2")
+ $sanitized_ingredients = array();
+ if ( isset( $instruction['ingredients'] ) && is_array( $instruction['ingredients'] ) ) {
+ foreach ( $instruction['ingredients'] as $ingredient ) {
+ $ingredient_str = (string) $ingredient;
+ // Check if it's a split (contains colon)
+ if ( strpos( $ingredient_str, ':' ) !== false ) {
+ // Split format: "uid:splitId" - validate and sanitize
+ $parts = explode( ':', $ingredient_str, 2 );
+ if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+ $sanitized_ingredients[] = intval( $parts[0] ) . ':' . intval( $parts[1] );
+ }
+ } else {
+ // Regular ingredient UID - convert to integer
+ $sanitized_ingredients[] = intval( $ingredient );
+ }
+ }
+ }
+
$sanitized_instruction = array(
'uid' => isset( $instruction['uid'] ) ? intval( $instruction['uid'] ) : -1,
'name' => isset( $instruction['name'] ) ? sanitize_text_field( $instruction['name'] ) : '',
'text' => isset( $instruction['text'] ) ? self::sanitize_html( $instruction['text'] ) : '',
'image' => isset( $instruction['image'] ) ? intval( $instruction['image'] ) : 0,
- 'ingredients' => isset( $instruction['ingredients'] ) ? array_map( 'intval', $instruction['ingredients'] ) : array(),
+ 'ingredients' => $sanitized_ingredients,
);
if ( isset( $instruction['video'] ) ) {
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-saver.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-saver.php
@@ -181,6 +181,7 @@
if ( isset( $recipe['author_display'] ) ) { $meta['wprm_author_display'] = $recipe['author_display']; }
if ( isset( $recipe['author_name'] ) ) { $meta['wprm_author_name'] = $recipe['author_name']; }
if ( isset( $recipe['author_link'] ) ) { $meta['wprm_author_link'] = $recipe['author_link']; }
+ if ( isset( $recipe['author_bio'] ) ) { $meta['wprm_author_bio'] = $recipe['author_bio']; }
if ( isset( $recipe['servings'] ) ) { $meta['wprm_servings'] = $recipe['servings']; }
if ( isset( $recipe['servings_unit'] ) ) { $meta['wprm_servings_unit'] = $recipe['servings_unit']; }
if ( isset( $recipe['servings_advanced_enabled'] ) ) { $meta['wprm_servings_advanced_enabled'] = $recipe['servings_advanced_enabled']; }
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-shell.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-shell.php
@@ -50,6 +50,7 @@
'author_display' => 'default',
'author_name' => 'custom' === WPRM_Settings::get( 'recipe_author_display_default' ) ? WPRM_Settings::get( 'recipe_author_custom_default' ) : '',
'author_link' => '',
+ 'author_bio' => '',
'rating' => false,
'servings' => 0,
'servings_unit' => '',
@@ -162,6 +163,15 @@
}
/**
+ * Get the recipe custom author bio.
+ *
+ * @since 9.6.0
+ */
+ public function custom_author_bio() {
+ return $this->meta( 'author_bio', '' );
+ }
+
+ /**
* Get the recipe image HTML.
*
* @since 5.2.0
--- a/wp-recipe-maker/includes/public/class-wprm-recipe.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe.php
@@ -80,6 +80,7 @@
$recipe['author_display'] = $this->author_display( true );
$recipe['author_name'] = $this->custom_author_name();
$recipe['author_link'] = $this->custom_author_link();
+ $recipe['author_bio'] = $this->custom_author_bio();
$recipe['cost'] = $this->cost();
$recipe['servings'] = $this->servings();
$recipe['servings_unit'] = $this->servings_unit();
@@ -366,6 +367,15 @@
}
/**
+ * Get the recipe custom author bio.
+ *
+ * @since 9.6.0
+ */
+ public function custom_author_bio() {
+ return $this->meta( 'wprm_author_bio', '' );
+ }
+
+ /**
* Get the recipe post author.
*
* @since 5.0.0
--- a/wp-recipe-maker/includes/public/class-wprm-shortcode-other.php
+++ b/wp-recipe-maker/includes/public/class-wprm-shortcode-other.php
@@ -97,7 +97,7 @@
}
$icon = sanitize_key( $atts['icon'] );
- $value = $atts['value'];
+ $value = sanitize_text_field( $atts['value'] );
$unit = strtoupper( sanitize_key( $atts['unit'] ) );
$help = sanitize_text_field( $atts['help'] );
@@ -217,20 +217,84 @@
// Get recipe (defaults to current).
$recipe = WPRM_Template_Shortcodes::get_recipe( $atts['id'] );
- if ( $recipe && is_numeric( $atts['uid'] ) ) {
- $uid = intval( $atts['uid'] );
+ if ( $recipe && $atts['uid'] ) {
+ $uid_str = (string) $atts['uid'];
+ $is_split = false;
+ $parent_uid = null;
+ $split_id = null;
+ $split_percentage = null;
+
+ // Check if this is a split (format: "uid:splitId")
+ if ( strpos( $uid_str, ':' ) !== false ) {
+ $is_split = true;
+ $parts = explode( ':', $uid_str, 2 );
+ if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+ $parent_uid = intval( $parts[0] );
+ $split_id = intval( $parts[1] );
+ } else {
+ // Invalid split format, fall back to text
+ return $output;
+ }
+ } else if ( is_numeric( $atts['uid'] ) ) {
+ $parent_uid = intval( $atts['uid'] );
+ } else {
+ // Invalid UID format, fall back to text
+ return $output;
+ }
$ingredients_flat = $recipe->ingredients_flat();
- $index = array_search( $uid, array_column( $ingredients_flat, 'uid' ) );
+ $index = array_search( $parent_uid, array_column( $ingredients_flat, 'uid' ) );
if ( false !== $index && isset( $ingredients_flat[ $index ] ) ) {
$found_ingredient = $ingredients_flat[ $index ];
if ( 'ingredient' === $found_ingredient['type'] ) {
+ // If this is a split, find the split and calculate amount
+ if ( $is_split ) {
+ $found_split = null;
+ if ( isset( $found_ingredient['splits'] ) && is_array( $found_ingredient['splits'] ) ) {
+ foreach ( $found_ingredient['splits'] as $split ) {
+ if ( isset( $split['id'] ) && intval( $split['id'] ) === $split_id && isset( $split['percentage'] ) ) {
+ $found_split = $split;
+ $split_percentage = floatval( $split['percentage'] );
+ break;
+ }
+ }
+ }
+
+ if ( ! $found_split ) {
+ // Split not found, fall back to text
+ return $output;
+ }
+
+ // Calculate split amount from parent amount and percentage
+ $parent_amount = isset( $found_ingredient['amount'] ) ? $found_ingredient['amount'] : '';
+ $parent_amount_parsed = WPRM_Recipe_Parser::parse_quantity( $parent_amount );
+
+ $split_amount = '';
+ if ( $parent_amount_parsed > 0 ) {
+ $split_amount_parsed = ( $parent_amount_parsed * $split_percentage ) / 100;
+
+ // Format the calculated amount
+ if ( $split_amount_parsed == floor( $split_amount_parsed ) ) {
+ $split_amount = (string) intval( $split_amount_parsed );
+ } else {
+ $split_amount = rtrim( rtrim( number_format( $split_amount_parsed, 2, '.', '' ), '0' ), '.' );
+ }
+ }
+
+ // Use parent's unit
+ $unit = isset( $found_ingredient['unit'] ) ? $found_ingredient['unit'] : '';
+ } else {
+ // Regular ingredient
+ $split_amount = isset( $found_ingredient['amount'] ) ? $found_ingredient['amount'] : '';
+ $unit = isset( $found_ingredient['unit'] ) ? $found_ingredient['unit'] : '';
+ }
+
$parts = array();
- if ( $found_ingredient['amount'] ) { $parts[] = $found_ingredient['amount']; };
- if ( $found_ingredient['unit'] ) { $parts[] = $found_ingredient['unit']; };
+ if ( $split_amount ) { $parts[] = $split_amount; };
+ if ( $unit ) { $parts[] = $unit; };
// Optionally add second unit system.
$show_both_units = 'both' === $atts['unit_conversion'];
@@ -264,9 +328,13 @@
$text_to_show = implode( ' ', $parts );
if ( $text_to_show ) {
+ // Use the full UID (including split ID) for the class
+ // Replace colon with dash in class name to avoid CSS selector issues
+ $uid_for_class = $is_split ? str_replace( ':', '-', $uid_str ) : $parent_uid;
+
$classes = array(
'wprm-inline-ingredient',
- 'wprm-inline-ingredient-' . $recipe->id() . '-' . $uid,
+ 'wprm-inline-ingredient-' . $recipe->id() . '-' . $uid_for_class,
'wprm-block-text-' . $atts['style'],
);
@@ -289,8 +357,14 @@
if ( '' !== $atts['notes_separator'] ) {
$data_keep_notes = ' data-notes-separator="' . esc_attr( $atts['notes_separator'] ) . '"';
}
+
+ // Add split data attributes for adjustable servings
+ $split_data_attr = '';
+ if ( $is_split && $split_percentage !== null ) {
+ $split_data_attr = ' data-split-percentage="' . esc_attr( $split_percentage ) . '" data-split-id="' . esc_attr( $split_id ) . '"';
+ }
- $output = '<span class="' . esc_attr( implode( ' ', $classes ) ) .'"' . $data_keep_notes . $style . '>' . $text_to_show . '</span>';
+ $output = '<span class="' . esc_attr( implode( ' ', $classes ) ) .'"' . $data_keep_notes . $split_data_attr . $style . '>' . $text_to_show . '</span>';
}
}
}
--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-author-bio.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-author-bio.php
@@ -81,15 +81,30 @@
$atts = parent::get_attributes( $atts );
$recipe = WPRM_Template_Shortcodes::get_recipe( $atts['id'] );
- $author = $recipe ? $recipe->post_author() : false;
+ if ( ! $recipe ) {
+ return apply_filters( parent::get_hook(), '', $atts );
+ }
- // Get author bio.
+ // Get author bio based on author display option.
$bio = false;
- if ( $author ) {
- $bio = get_the_author_meta( 'description', $author );
+ $author_display = $recipe->author_display();
+
+ switch ( $author_display ) {
+ case 'post_author':
+ $author = $recipe->post_author();
+ if ( $author ) {
+ $bio = get_the_author_meta( 'description', $author );
+ }
+ break;
+ case 'custom':
+ $bio = $recipe->custom_author_bio();
+ break;
+ case 'same':
+ $bio = WPRM_Settings::get( 'recipe_author_same_bio' );
+ break;
}
- if ( ! $recipe || ! $bio ) {
+ if ( ! $bio ) {
return apply_filters( parent::get_hook(), '', $atts );
}
--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-counter.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-counter.php
@@ -87,17 +87,40 @@
// Check if %count% or %total% is used - replace with placeholders that will be replaced later.
// This ensures we count only rendered elements, not items processed by other plugins.
$needs_replacement = false;
- if ( false !== stripos( $text, '%count%' ) ) {
- $text = str_ireplace( '%count%', '<span class="wprm-recipe-counter-count">1</span>', $text );
- $needs_replacement = true;
- }
- if ( false !== stripos( $text, '%total%' ) ) {
- $text = str_ireplace( '%total%', '<span class="wprm-recipe-counter-total">1</span>', $text );
- $needs_replacement = true;
- }
- if ( $needs_replacement ) {
- $GLOBALS['wprm_recipe_counter_needs_replacement'] = true;
+ // In Gutenberg preview (backend editor), use stored block position if available.
+ if ( WPRM_Context::is_gutenberg_preview() && isset( $GLOBALS['wprm_roundup_block_position'] ) ) {
+ $block_position = intval( $GLOBALS['wprm_roundup_block_position'] );
+
+ // Ensure we have a valid position (at least 1).
+ if ( $block_position < 1 ) {
+ $block_position = 1;
+ }
+
+ // Replace %count% with the actual position in backend.
+ if ( false !== stripos( $text, '%count%' ) ) {
+ $text = str_ireplace( '%count%', $block_position, $text );
+ }
+
+ // For %total%, we still need to calculate it from the post content.
+ if ( false !== stripos( $text, '%total%' ) ) {
+ $total_count = self::get_roundup_total_count();
+ $text = str_ireplace( '%total%', $total_count, $text );
+ }
+ } else {
+ // Frontend or backend without position: use placeholder replacement that happens later via filter.
+ if ( false !== stripos( $text, '%count%' ) ) {
+ $text = str_ireplace( '%count%', '<span class="wprm-recipe-counter-count">1</span>', $text );
+ $needs_replacement = true;
+ }
+ if ( false !== stripos( $text, '%total%' ) ) {
+ $text = str_ireplace( '%total%', '<span class="wprm-recipe-counter-total">1</span>', $text );
+ $needs_replacement = true;
+ }
+
+ if ( $needs_replacement ) {
+ $GLOBALS['wprm_recipe_counter_needs_replacement'] = true;
+ }
}
if ( $atts['link'] && $recipe->permalink() ) {
@@ -115,6 +138,135 @@
$output = '<' . $tag . ' class="' . esc_attr( implode( ' ', $classes ) ) . '">' . $text . '</' . $tag . '>';
return apply_filters( parent::get_hook(), $output, $atts, $recipe );
}
+
+ /**
+ * Get the total count of roundup items in the post content.
+ * Used in backend editor to display correct total.
+ *
+ * @since 10.0.0
+ * @return int Total count of roundup items.
+ */
+ private static function get_roundup_total_count() {
+ static $total_count_cache = null;
+
+ if ( null !== $total_count_cache ) {
+ return $total_count_cache;
+ }
+
+ $total_count = 0;
+
+ // Try to get post content from various sources.
+ $post_content = '';
+ $post_id = 0;
+
+ // Try to get from global post.
+ if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+ $post_id = $GLOBALS['post']->ID;
+ $post_content = $GLOBALS['post']->post_content;
+ }
+
+ // Try to get from request parameters (REST API).
+ if ( empty( $post_content ) ) {
+ // Check various possible parameter names.
+ $possible_params = array( 'post_id', 'postId', 'context[postId]' );
+ foreach ( $possible_params as $param ) {
+ if ( isset( $_REQUEST[ $param ] ) ) {
+ $post_id = intval( $_REQUEST[ $param ] );
+ break;
+ }
+ }
+
+ // Also check JSON body for REST API requests.
+ if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+ $json_input = file_get_contents( 'php://input' );
+ if ( ! empty( $json_input ) ) {
+ $json_data = json_decode( $json_input, true );
+ if ( $json_data ) {
+ if ( isset( $json_data['post_id'] ) ) {
+ $post_id = intval( $json_data['post_id'] );
+ } elseif ( isset( $json_data['context']['postId'] ) ) {
+ $post_id = intval( $json_data['context']['postId'] );
+ }
+ }
+ }
+ }
+
+ if ( $post_id ) {
+ $post = get_post( $post_id );
+ if ( $post ) {
+ $post_content = $post->post_content;
+ }
+ }
+ }
+
+ // Count roundup item blocks in the content.
+ if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+ $blocks = parse_blocks( $post_content );
+ if ( ! empty( $blocks ) ) {
+ $total_count = self::count_roundup_blocks( $blocks );
+ }
+ }
+
+ // Fallback to 1 if no items found.
+ if ( 0 === $total_count ) {
+ $total_count = 1;
+ }
+
+ $total_count_cache = $total_count;
+ return $total_count;
+ }
+
+ /**
+ * Recursively count roundup item blocks in parsed blocks.
+ * Also counts roundup items within list blocks.
+ *
+ * @since 10.0.0
+ * @param array $blocks Parsed block list.
+ * @return int Count of roundup item blocks.
+ */
+ private static function count_roundup_blocks( $blocks ) {
+ $count = 0;
+
+ foreach ( $blocks as $block ) {
+ if ( ! is_array( $block ) ) {
+ continue;
+ }
+
+ // Check if this is a roundup item block.
+ if ( isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+ $count++;
+ }
+
+ // Check if this is a list block and count roundup items within it.
+ if ( isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+ $attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+ $list_id = isset( $attrs['id'] ) ? intval( $attrs['id'] ) : 0;
+ if ( $list_id ) {
+ // Count roundup items in this list.
+ if ( class_exists( 'WPRM_List_Manager' ) ) {
+ $list = WPRM_List_Manager::get_list( $list_id );
+ if ( $list && method_exists( $list, 'items' ) ) {
+ $items = $list->items();
+ if ( is_array( $items ) ) {
+ foreach ( $items as $item ) {
+ if ( is_array( $item ) && isset( $item['type'] ) && 'roundup' === $item['type'] ) {
+ $count++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Recursively count inner blocks.
+ if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+ $count += self::count_roundup_blocks( $block['innerBlocks'] );
+ }
+ }
+
+ return $count;
+ }
}
WPRM_SC_Counter::init();
No newline at end of file
--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-instructions.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-instructions.php
@@ -1415,16 +1415,87 @@
$ingredients_flat = $recipe->ingredients_flat();
foreach ( $instruction['ingredients'] as $ingredient ) {
- $index = array_search( $ingredient, array_column( $ingredients_flat, 'uid' ) );
+ $ingredient_str = (string) $ingredient;
+ $is_split = false;
+ $parent_uid = null;
+ $split_id = null;
+
+ // Check if this is a split (format: "uid:splitId")
+ if ( strpos( $ingredient_str, ':' ) !== false ) {
+ $is_split = true;
+ $parts = explode( ':', $ingredient_str, 2 );
+ if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+ $parent_uid = intval( $parts[0] );
+ $split_id = intval( $parts[1] );
+ } else {
+ // Invalid split format, skip
+ continue;
+ }
+ } else {
+ // Regular ingredient UID
+