This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,36 @@
<?php
/**
* Handles the accordion block.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
require_once 'class-accordion.php';
require_once 'class-accordion-item.php';
require_once 'class-accordion-toggle.php';
require_once 'class-accordion-toggle-icon.php';
require_once 'class-accordion-content.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_accordion_block_editor_settings', 20 );
/**
* Add block editor settings for the navigation block.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_accordion_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-accordion',
'.editor-styles-wrapper .wp-block-generateblocks-pro-accordion-item',
'.editor-styles-wrapper .wp-block-generateblocks-pro-accordion-toggle',
'.editor-styles-wrapper .wp-block-generateblocks-pro-accordion-toggle-icon',
'.editor-styles-wrapper .wp-block-generateblocks-pro-accordion-content',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Accordion block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion block class.
*/
class GenerateBlocks_Block_Accordion_Content extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/accordion-content';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Accordion block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion block class.
*/
class GenerateBlocks_Block_Accordion_Item extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/accordion-item';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Accordion block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion block class.
*/
class GenerateBlocks_Block_Accordion_Toggle_Icon extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/accordion-toggle-icon';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* Handles the Accordion block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion block class.
*/
class GenerateBlocks_Block_Accordion_Toggle extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/accordion-toggle';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
// Bail here if the HTML Tag Processor isn't available.
if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
return $block_content;
}
$open_by_default = $block->context['generateblocks/accordion/openByDefault'] ?? false;
$processor = new WP_HTML_Tag_Processor( $block_content );
$tag_name = $attributes['tagName'] ?? 'div';
$main_tag = $processor->next_tag( $tag_name );
$updated_html = false;
if ( $processor && $main_tag ) {
if ( $open_by_default ) {
$processor->add_class( 'gb-block-is-current' );
$updated_html = true;
}
if ( 'div' === $tag_name ) {
if ( ! $processor->get_attribute( 'tabindex' ) ) {
$processor->set_attribute( 'tabindex', '0' );
}
if ( ! $processor->get_attribute( 'role' ) ) {
$processor->set_attribute( 'role', 'button' );
}
$updated_html = true;
}
if ( $updated_html ) {
$block_content = $processor->get_updated_html();
}
}
return $block_content;
}
}
@@ -0,0 +1,91 @@
<?php
/**
* Handles the Accordion block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion block class.
*/
class GenerateBlocks_Block_Accordion extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/accordion';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( ! wp_style_is( 'generateblocks-accordion', 'enqueued' ) ) {
self::enqueue_style();
}
if ( ! wp_script_is( 'generateblocks-accordion', 'enqueued' ) ) {
self::enqueue_assets();
}
return $block_content;
}
/**
* Enqueue block styles.
*/
private static function enqueue_style() {
wp_enqueue_style(
'generateblocks-accordion',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/accordion-style.css',
[],
GENERATEBLOCKS_PRO_VERSION
);
}
/**
* Enqueue block scripts.
*/
private static function enqueue_scripts() {
wp_enqueue_script(
'generateblocks-accordion',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/accordion.js',
[],
GENERATEBLOCKS_PRO_VERSION,
true
);
}
/**
* Enqueue block assets.
*/
public static function enqueue_assets() {
self::enqueue_scripts();
self::enqueue_style();
}
}
@@ -0,0 +1,233 @@
<?php
/**
* Register our blocks.
*
* @package GenerateBlocks Pro
*/
add_action( 'init', 'generateblocks_pro_register_blocks' );
/**
* Register the GenerateBlocks Pro blocks.
*
* @since 1.0.0
*/
function generateblocks_pro_register_blocks() {
if ( ! class_exists( 'GenerateBlocks_Block' ) ) {
return;
}
require_once 'accordion/accordion.php';
require_once 'tabs/tabs.php';
require_once 'classic-menu/classic-menu.php';
require_once 'classic-menu-item/classic-menu-item.php';
require_once 'classic-sub-menu/classic-sub-menu.php';
require_once 'navigation/navigation.php';
require_once 'menu-container/menu-container.php';
require_once 'menu-toggle/menu-toggle.php';
require_once 'site-header/site-header.php';
require_once 'carousel/carousel.php';
require_once 'carousel-pagination/carousel-pagination.php';
require_once 'carousel-items/carousel-items.php';
require_once 'carousel-item/carousel-item.php';
require_once 'carousel-control/carousel-control.php';
if ( generateblocks_pro_forms_enabled() ) {
require_once 'form/form.php';
require_once 'form-render/form-render.php';
require_once 'form-field/form-field.php';
require_once 'form-field-label/form-field-label.php';
require_once 'form-field-control/form-field-control.php';
}
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/accordion',
[
'render_callback' => 'GenerateBlocks_Block_Accordion::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/accordion-item',
[
'render_callback' => 'GenerateBlocks_Block_Accordion_Item::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/accordion-toggle',
[
'render_callback' => 'GenerateBlocks_Block_Accordion_Toggle::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/accordion-toggle-icon',
[
'render_callback' => 'GenerateBlocks_Block_Accordion_Toggle_Icon::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/accordion-content',
[
'render_callback' => 'GenerateBlocks_Block_Accordion_Content::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/tabs',
[
'render_callback' => 'GenerateBlocks_Block_Tabs::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/tabs-menu',
[
'render_callback' => 'GenerateBlocks_Block_Tabs_Menu::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/tab-menu-item',
[
'render_callback' => 'GenerateBlocks_Block_Tab_Menu_Item::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/tab-items',
[
'render_callback' => 'GenerateBlocks_Block_Tab_Items::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/tab-item',
[
'render_callback' => 'GenerateBlocks_Block_Tab_Item::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/classic-menu',
[
'render_callback' => 'GenerateBlocks_Block_Classic_Menu::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/classic-menu-item',
[
'render_callback' => 'GenerateBlocks_Block_Classic_Menu_Item::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/classic-sub-menu',
[
'render_callback' => 'GenerateBlocks_Block_Classic_Sub_Menu::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/navigation',
[
'render_callback' => 'GenerateBlocks_Block_Navigation::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/menu-toggle',
[
'render_callback' => 'GenerateBlocks_Block_Menu_Toggle::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/menu-container',
[
'render_callback' => 'GenerateBlocks_Block_Menu_Container::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/site-header',
[
'render_callback' => 'GenerateBlocks_Block_Site_Header::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/carousel',
[
'render_callback' => 'GenerateBlocks_Block_Carousel::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/carousel-pagination',
[
'render_callback' => 'GenerateBlocks_Block_Carousel_Pagination::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/carousel-items',
[
'render_callback' => 'GenerateBlocks_Block_Carousel_Items::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/carousel-item',
[
'render_callback' => 'GenerateBlocks_Block_Carousel_Item::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/carousel-control',
[
'render_callback' => 'GenerateBlocks_Block_Carousel_Control::render_block',
]
);
if ( generateblocks_pro_forms_enabled() ) {
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/form',
[
'render_callback' => 'GenerateBlocks_Block_Form::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/form-field',
[
'render_callback' => 'GenerateBlocks_Block_Form_Field::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/form-field-label',
[
'render_callback' => 'GenerateBlocks_Block_Form_Field_Label::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/form-field-control',
[
'render_callback' => 'GenerateBlocks_Block_Form_Field_Control::render_block',
]
);
register_block_type_from_metadata(
GENERATEBLOCKS_PRO_DIR . '/dist/blocks/form-render',
[
'render_callback' => 'GenerateBlocks_Block_Form_Render::render_block',
]
);
}
}
@@ -0,0 +1,13 @@
<?php
/**
* Registers the Carousel Control block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Load the class file.
require_once GENERATEBLOCKS_PRO_DIR . 'includes/blocks/carousel-control/class-carousel-control.php';
@@ -0,0 +1,86 @@
<?php
/**
* Handles the Carousel Control block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Carousel Control block class.
*/
class GenerateBlocks_Block_Carousel_Control extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/carousel-control';
/**
* Render the Carousel Control block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
// Add default ARIA labels if not present using WP HTML Tag Processor
// Only available in WordPress 6.2+, skip for older versions.
if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
$control_type = $attributes['controlType'] ?? 'next';
$has_aria_label = isset( $attributes['htmlAttributes']['aria-label'] ) && ! empty( $attributes['htmlAttributes']['aria-label'] );
$processor = new \WP_HTML_Tag_Processor( $block_content );
// Find the first tag (the button/control element).
if ( $processor->next_tag() ) {
// Add default ARIA label if not present.
if ( ! $has_aria_label && ! $processor->get_attribute( 'aria-label' ) ) {
// Map control types to default ARIA labels.
$default_labels = [
'next' => __( 'Next slide', 'generateblocks-pro' ),
'previous' => __( 'Previous slide', 'generateblocks-pro' ),
'play' => __( 'Play carousel', 'generateblocks-pro' ),
'pause' => __( 'Pause carousel', 'generateblocks-pro' ),
'play-pause' => __( 'Toggle carousel playback', 'generateblocks-pro' ),
'first' => __( 'Go to first slide', 'generateblocks-pro' ),
'last' => __( 'Go to last slide', 'generateblocks-pro' ),
];
$aria_label = $default_labels[ $control_type ] ?? __( 'Carousel control', 'generateblocks-pro' );
$processor->set_attribute( 'aria-label', $aria_label );
}
// For play-pause controls, ensure aria-pressed attribute exists.
if ( 'play-pause' === $control_type && ! $processor->get_attribute( 'aria-pressed' ) ) {
$processor->set_attribute( 'aria-pressed', 'false' );
}
$block_content = $processor->get_updated_html();
}
}
return $block_content;
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Handles the carousel item block.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
require_once 'class-carousel-item.php';
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Carousel Item block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Carousel Item block class.
*/
class GenerateBlocks_Block_Carousel_Item extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/carousel-item';
/**
* Render the Carousel Item block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the carousel block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-carousel-items.php';
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Carousel block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Carousel block class.
*/
class GenerateBlocks_Block_Carousel_Items extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/carousel-items';
/**
* Render the Carousel block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the carousel pagination block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-carousel-pagination.php';
@@ -0,0 +1,97 @@
<?php
/**
* Handles the Carousel Pagination block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Carousel Pagination block class.
*/
class GenerateBlocks_Block_Carousel_Pagination extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/carousel-pagination';
/**
* Render the Carousel Pagination block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
$to_replace = '<span class="gb-carousel-pagination-content"></span>';
$pagination_type = $block->context['generateblocks/carousel/htmlAttributes']['data-pagination-type'] ?? 'bullets';
$replacement = '';
if ( 'fraction' === $pagination_type ) {
$replacement = '<span class="gb-carousel-current">1</span> / <span class="gb-carousel-total">1</span>';
}
if ( 'bullets' === $pagination_type ) {
$replacement = '<span class="gb-carousel-dot" style="display: inline-block !important;"></span>';
}
if ( $replacement ) {
$block_content = str_replace( $to_replace, $replacement, $block_content );
}
// Add default ARIA attributes if not present using WP HTML Tag Processor
// Only available in WordPress 6.2+, skip for older versions.
if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
$has_aria_label = isset( $attributes['htmlAttributes']['aria-label'] ) && ! empty( $attributes['htmlAttributes']['aria-label'] );
$processor = new \WP_HTML_Tag_Processor( $block_content );
// Find the first tag (the pagination container).
if ( $processor->next_tag() ) {
// Add default ARIA label if not present.
if ( ! $has_aria_label && ! $processor->get_attribute( 'aria-label' ) ) {
// Map pagination types to default ARIA labels.
$default_labels = [
'bullets' => __( 'Carousel pagination', 'generateblocks-pro' ),
'fraction' => __( 'Carousel slide counter', 'generateblocks-pro' ),
'progressbar' => __( 'Carousel progress', 'generateblocks-pro' ),
];
$aria_label = $default_labels[ $pagination_type ] ?? __( 'Carousel pagination', 'generateblocks-pro' );
$processor->set_attribute( 'aria-label', $aria_label );
}
// Add role attribute for bullets pagination if not present.
if ( 'bullets' === $pagination_type && ! $processor->get_attribute( 'role' ) ) {
$processor->set_attribute( 'role', 'navigation' );
}
$block_content = $processor->get_updated_html();
}
}
return $block_content;
}
}
@@ -0,0 +1,127 @@
<?php
/**
* Handle the carousel block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-carousel.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_carousel_block_editor_settings', 20 );
/**
* Add block editor settings for the carousel block.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_carousel_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-carousel',
'.editor-styles-wrapper .wp-block-generateblocks-pro-carousel-pagination',
'.editor-styles-wrapper .wp-block-generateblocks-pro-carousel-items',
'.editor-styles-wrapper .wp-block-generateblocks-pro-carousel-item',
'.editor-styles-wrapper .wp-block-generateblocks-pro-carousel-control',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
add_filter( 'generateblocks_block_css', 'generateblocks_pro_carousel_block_css', 10, 2 );
/**
* Add block CSS for the carousel block.
*
* @param string $css The CSS to add.
* @param array $block The block data.
*
* @return string
*/
function generateblocks_pro_carousel_block_css( $css, $block ) {
if ( ! isset( $block['attributes']['uniqueId'] ) ) {
return $css;
}
$block_name = $block['block_name'] ?? '';
if ( 'generateblocks-pro/carousel' !== $block_name ) {
return $css;
}
$selector = '.gb-carousel-' . $block['attributes']['uniqueId'];
$init_at_raw = $block['attributes']['htmlAttributes']['data-init-at'] ?? '';
$init_at = '';
if ( is_string( $init_at_raw ) ) {
$init_at_raw = trim( $init_at_raw );
if ( '' !== $init_at_raw && false === strpbrk( $init_at_raw, '{};()/*' ) ) {
$init_at = $init_at_raw;
if ( is_numeric( $init_at ) ) {
$init_at .= 'px';
}
}
}
if ( $init_at ) {
$css_rules = [
$selector . ':not([data-gb-carousel-initialized="true"]):not([data-gb-carousel-pending="true"]) .gb-carousel-items' => [
'position' => 'relative',
'width' => '100%',
'height' => '100%',
'z-index' => '1',
'display' => 'flex',
'overflow-x' => 'hidden',
'gap' => 'var(--gb-carousel-slide-gap, 0)',
],
$selector . ':not([data-gb-carousel-initialized="true"]) .gb-carousel-items > .gb-carousel-item' => [
'flex' => '0 0 calc((100% - var(--gb-carousel-slide-gap, 0px) * (var(--gb-carousel-slides-per-view, 1) - 1)) / var(--gb-carousel-slides-per-view, 1))',
'width' => '100%',
'height' => '100%',
'position' => 'relative',
'transition-property' => 'transform',
'display' => 'block',
'user-select' => 'none',
],
];
$media_query_css = generateblocks_pro_build_css_from_array( $css_rules );
$css .= "@media (max-width: {$init_at}) {{$media_query_css}}";
$not_at_rule_css_rules = [
$selector . ':not([data-gb-carousel-initialized="true"]) .gb-carousel-control' => [
'display' => 'none',
],
$selector . ':not([data-gb-carousel-initialized="true"]) .gb-carousel-pagination' => [
'display' => 'none',
],
];
$not_at_rule_css = generateblocks_pro_build_css_from_array( $not_at_rule_css_rules );
$css .= "@media (width > {$init_at}) {{$not_at_rule_css}}";
}
$grid_rows = $block['attributes']['htmlAttributes']['data-grid-rows'] ?? '';
if ( $grid_rows ) {
$slides_per_view = $block['attributes']['htmlAttributes']['data-slides-per-view'] ?? '1';
$total_items = $grid_rows * $slides_per_view;
$selected_items = $total_items + 1;
$css_rules = [
$selector . ':not([data-gb-carousel-initialized="true"]):not([data-gb-carousel-pending="true"]) > .gb-carousel-items > .gb-carousel-item:nth-child(n+' . $selected_items . ')' => [
'display' => 'none',
],
];
$built_css = generateblocks_pro_build_css_from_array( $css_rules );
if ( $init_at ) {
$css .= "@media (max-width: {$init_at}) {{$built_css}}";
} else {
$css .= $built_css;
}
}
return $css;
}
@@ -0,0 +1,87 @@
<?php
/**
* Handles the Carousel block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Carousel block class.
*/
class GenerateBlocks_Block_Carousel extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/carousel';
/**
* Render the Carousel block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Enqueue frontend scripts and styles.
self::enqueue_scripts();
self::enqueue_styles();
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
/**
* Enqueue frontend scripts for the carousel.
*/
private static function enqueue_scripts() {
wp_enqueue_script(
'generateblocks-carousel',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/carousel.js',
[],
GENERATEBLOCKS_PRO_VERSION,
true
);
}
/**
* Enqueue frontend styles for the carousel.
*/
private static function enqueue_styles() {
wp_enqueue_style(
'generateblocks-carousel',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/carousel-style.css',
[],
GENERATEBLOCKS_PRO_VERSION
);
}
/**
* Enqueue block assets.
*/
public static function enqueue_assets() {
self::enqueue_scripts();
self::enqueue_styles();
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Classic_Menu_Item extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/classic-menu-item';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the menu block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-classic-menu-item.php';
@@ -0,0 +1,344 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Classic_Menu extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/classic-menu';
/**
* Cache for mega menu display checks.
*
* @var array
*/
private static $mega_menu_cache = [];
/**
* Check if a mega menu overlay should display based on conditions.
*
* @param int $overlay_id The overlay post ID.
* @return bool Whether the overlay should display.
*/
private static function should_display_mega_menu( $overlay_id ) {
// Return false if overlays are disabled.
if ( ! generateblocks_pro_overlays_enabled() ) {
return false;
}
if ( ! $overlay_id ) {
return false;
}
// Check cache first.
if ( isset( self::$mega_menu_cache[ $overlay_id ] ) ) {
return self::$mega_menu_cache[ $overlay_id ];
}
// Check if post exists and is published.
$overlay_post = get_post( $overlay_id );
if ( ! $overlay_post || 'publish' !== $overlay_post->post_status ) {
self::$mega_menu_cache[ $overlay_id ] = false;
return false;
}
$overlay_post_content = do_blocks( $overlay_post->post_content ) ?? '';
if ( empty( $overlay_post_content ) ) {
self::$mega_menu_cache[ $overlay_id ] = false;
return false;
}
// Check display conditions.
$display_condition = get_post_meta( $overlay_id, '_gb_overlay_display_condition', true );
$display_conditions = [];
if ( $display_condition ) {
// Check if the condition post exists and is published.
$condition_post = get_post( $display_condition );
if ( $condition_post && 'publish' === $condition_post->post_status ) {
$display_conditions = get_post_meta( $display_condition, '_gb_conditions', true ) ?? [];
}
}
$show = true;
if ( ! empty( $display_conditions ) ) {
$show = GenerateBlocks_Pro_Conditions::show( $display_conditions );
$invert_condition = GenerateBlocks_Pro_Overlays::get_overlay_meta( $overlay_id, '_gb_overlay_display_condition_invert' );
// If invert is enabled, flip the result.
if ( $invert_condition ) {
$show = ! $show;
}
}
// Cache the result.
self::$mega_menu_cache[ $overlay_id ] = $show;
return $show;
}
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Clear mega menu cache for fresh checks.
self::$mega_menu_cache = [];
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
$selected_menu = $attributes['menu'] ?? '';
$unique_id = $attributes['uniqueId'] ?? '';
if ( ! $selected_menu || ! $unique_id ) {
return;
}
if ( isset( $block->context['generateblocks-pro/subMenuType'] ) ) {
$sub_menu_type = $block->context['generateblocks-pro/subMenuType'];
} elseif ( isset( $_GET['subMenuType'] ) ) { // phpcs:ignore -- No processing of data.
$sub_menu_type = esc_attr( $_GET['subMenuType'] ); // phpcs:ignore -- No processing of data.
} else {
$sub_menu_type = 'hover';
}
$classes = [
'gb-menu',
'gb-menu--base',
'gb-menu-' . $unique_id,
'gb-menu--' . $sub_menu_type,
];
if ( ! empty( $attributes['globalClasses'] ) ) {
$classes = array_merge( $classes, $attributes['globalClasses'] );
}
if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
$class = implode( ' ', $classes );
$add_menu_item_classes = function( $classes, $menu_item ) use ( $sub_menu_type, $unique_id ) {
$mega_menu = get_post_meta( $menu_item->ID, '_gb_mega_menu', true );
if ( $mega_menu && self::should_display_mega_menu( $mega_menu ) ) {
$classes[] = 'menu-item-has-gb-mega-menu';
$classes[] = 'menu-item-has-children';
}
$classes[] = 'gb-menu-item';
$new_unique_id = substr_replace( $unique_id, 'mi', 0, 2 );
$classes[] = 'gb-menu-item-' . $new_unique_id;
// Escape classes.
$classes = array_map( 'esc_attr', $classes );
return $classes;
};
$add_dropdown_icon = function( $title, $menu_item ) use ( $sub_menu_type ) {
$mega_menu = get_post_meta( $menu_item->ID, '_gb_mega_menu', true );
$has_children = in_array( 'menu-item-has-children', $menu_item->classes, true );
$show_mega_menu = $mega_menu && self::should_display_mega_menu( $mega_menu );
if ( $show_mega_menu || $has_children ) {
$modal_id = $show_mega_menu ? 'gb-overlay-' . $mega_menu : '';
$arrow_icon = '<svg class="gb-submenu-toggle-icon" viewBox="0 0 330 512" aria-hidden="true" width="1em" height="1em" fill="currentColor"><path d="M305.913 197.085c0 2.266-1.133 4.815-2.833 6.514L171.087 335.593c-1.7 1.7-4.249 2.832-6.515 2.832s-4.815-1.133-6.515-2.832L26.064 203.599c-1.7-1.7-2.832-4.248-2.832-6.514s1.132-4.816 2.832-6.515l14.162-14.163c1.7-1.699 3.966-2.832 6.515-2.832 2.266 0 4.815 1.133 6.515 2.832l111.316 111.317 111.316-111.317c1.7-1.699 4.249-2.832 6.515-2.832s4.815 1.133 6.515 2.832l14.162 14.163c1.7 1.7 2.833 4.249 2.833 6.515z"></path></svg>';
$submenu_button = sprintf(
'<span class="gb-submenu-toggle" aria-label="%3$s" role="button" aria-expanded="false" aria-haspopup="menu" tabindex="0"%2$s>%1$s</span>',
$arrow_icon,
$modal_id ? ' data-gb-overlay="' . esc_attr( $modal_id ) . '" data-gb-overlay-trigger-type="click" aria-controls="' . esc_attr( $modal_id ) . '"' : '',
sprintf(
/* translators: %s: Menu item title. */
esc_attr__( '%s Sub-Menu', 'generateblocks-pro' ),
esc_attr( wp_strip_all_tags( $title ) )
)
);
if ( 'click' === $sub_menu_type ) {
return $title . $arrow_icon;
}
return $title . $submenu_button;
}
return $title;
};
$add_link_atts = function ( $atts, $menu_item ) use ( $sub_menu_type ) {
$class = $atts['class'] ?? '';
$class .= ' gb-menu-link';
$class = trim( $class );
$atts['class'] = esc_attr( $class );
$disable_links = isset( $_GET['disableLinks'] ); // phpcs:ignore -- No processing of data.
if ( $disable_links ) {
$atts['onClick'] = 'event.preventDefault();';
}
if ( 'hover' === $sub_menu_type || 'click' === $sub_menu_type ) {
$mega_menu = get_post_meta( $menu_item->ID, '_gb_mega_menu', true );
$show_mega_menu = $mega_menu && self::should_display_mega_menu( $mega_menu );
if ( $show_mega_menu ) {
$modal_id = 'gb-overlay-' . $mega_menu;
// Add directive and modal element target.
$atts['data-gb-overlay'] = $modal_id;
if ( 'hover' === $sub_menu_type ) {
$atts['data-gb-overlay-trigger-type'] = 'hover';
}
$atts['aria-controls'] = $modal_id;
}
if ( $show_mega_menu || in_array( 'menu-item-has-children', $menu_item->classes, true ) ) {
if ( 'click' === $sub_menu_type ) {
$atts['role'] = 'button';
$atts['aria-expanded'] = 'false';
$atts['aria-label'] = sprintf(
/* translators: %s: Menu item title. */
esc_attr__( '%s Sub-Menu', 'generateblocks-pro' ),
esc_attr( wp_strip_all_tags( $menu_item->title ?? '' ) )
);
}
$atts['aria-haspopup'] = 'menu';
}
}
return $atts;
};
$add_sub_menu_attributes = function( $atts, $args ) use ( $unique_id ) {
$new_unique_id = substr_replace( $unique_id, 'sm', 0, 2 );
$atts['class'] = 'sub-menu gb-sub-menu gb-sub-menu-' . $new_unique_id;
return $atts;
};
$add_mega_menu = function( $item_output, $item ) {
$item_id = $item->ID;
$mega_menu = get_post_meta( $item_id, '_gb_mega_menu', true );
if ( ! $mega_menu || ! self::should_display_mega_menu( $mega_menu ) ) {
return $item_output;
}
$action_id = 'gb-mega-menu-' . $mega_menu;
ob_start();
do_action( $action_id, $item );
$mega_menu_content = ob_get_clean();
return $item_output . $mega_menu_content;
};
add_filter( 'nav_menu_css_class', $add_menu_item_classes, 10, 2 );
add_filter( 'nav_menu_submenu_attributes', $add_sub_menu_attributes, 10, 2 );
add_filter( 'nav_menu_item_title', $add_dropdown_icon, 10, 2 );
add_filter( 'nav_menu_link_attributes', $add_link_atts, 10, 2 );
add_filter( 'walker_nav_menu_start_el', $add_mega_menu, 10, 2 );
ob_start();
wp_nav_menu(
[
'menu' => $selected_menu,
'container' => '',
'container_class' => '',
'menu_class' => $class,
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
'fallback_cb' => false,
]
);
remove_filter( 'nav_menu_css_class', $add_menu_item_classes, 10, 2 );
remove_filter( 'nav_menu_submenu_attributes', $add_sub_menu_attributes, 10, 2 );
remove_filter( 'nav_menu_item_title', $add_dropdown_icon, 10, 2 );
remove_filter( 'nav_menu_link_attributes', $add_link_atts, 10, 2 );
remove_filter( 'walker_nav_menu_start_el', $add_mega_menu, 10, 2 );
do_action( 'generateblocks_pro_after_menu_block', $attributes, is_admin() );
$block_content .= ob_get_clean();
if ( ! wp_style_is( 'generateblocks-classic-menu', 'enqueued' ) ) {
self::enqueue_style();
}
if ( ! wp_script_is( 'generateblocks-classic-menu', 'enqueued' ) ) {
self::enqueue_scripts();
}
return $block_content;
}
/**
* Enqueue block styles.
*/
private static function enqueue_style() {
wp_enqueue_style(
'generateblocks-classic-menu',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/classic-menu-style.css',
[],
GENERATEBLOCKS_PRO_VERSION
);
}
/**
* Enqueue block scripts.
*/
private static function enqueue_scripts() {
wp_enqueue_script(
'generateblocks-classic-menu',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/classic-menu.js',
[],
GENERATEBLOCKS_PRO_VERSION,
true
);
}
/**
* Enqueue block assets.
*/
public static function enqueue_assets() {
self::enqueue_scripts();
self::enqueue_style();
}
}
@@ -0,0 +1,33 @@
<?php
/**
* Handle the menu block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-classic-menu.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_classic_menu_block_settings' );
/**
* Add menu block settings.
* We need to add CSS this way as it prepends `.editor-styles-wrapper` to the selectors.
* Including the stylesheet via block.json does not, which causes specificity issues.
*
* @param array $settings Block editor settings.
* @return array
*/
function generateblocks_pro_classic_menu_block_settings( $settings ) {
$navigation_css = file_get_contents( GENERATEBLOCKS_PRO_DIR . 'dist/classic-menu-style.css' ); // phpcs:ignore -- correct function to use.
if ( ! $navigation_css ) {
return $settings;
}
$settings['styles'][] = [
'css' => $navigation_css,
'source' => 'generateblocks-pro/class-menu-style',
];
return $settings;
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Classic_Sub_Menu extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/classic-sub-menu';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the menu block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-classic-sub-menu.php';
@@ -0,0 +1,627 @@
<?php
/**
* Handles the Form Field Control block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form field control block class.
*/
class GenerateBlocks_Block_Form_Field_Control extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array
*/
protected static $block_ids = [];
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form-field-control';
/**
* Maximum textarea rows accepted from block attributes.
*
* @var int
*/
const MAX_TEXTAREA_ROWS = 100;
/**
* Default allow-list for custom form control HTML attributes.
*
* Prefix entries ending in `-*` match all attributes with that prefix.
*
* @var array
*/
private static $allowed_html_attributes = [
'aria-*',
'data-*',
'autocapitalize',
'autocomplete',
'enterkeyhint',
'inputmode',
'max',
'maxlength',
'min',
'minlength',
'pattern',
'spellcheck',
'step',
'title',
];
/**
* Attributes generated by the renderer or controlled by runtime behavior.
*
* @var array
*/
private static $reserved_html_attributes = [
'aria-invalid',
'aria-required',
'checked',
'class',
'disabled',
'id',
'multiple',
'name',
'placeholder',
'readonly',
'required',
'rows',
'selected',
'style',
'type',
'value',
];
/**
* Field types where custom HTML attributes apply to one rendered control.
*
* @var array
*/
private static $custom_html_attribute_field_types = [
'text',
'email',
'url',
'tel',
'number',
'textarea',
'select',
'checkbox',
];
/**
* Field-type support for known default attributes.
*
* @var array
*/
private static $html_attribute_field_type_support = [
'autocapitalize' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
'enterkeyhint' => [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ],
'inputmode' => [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ],
'max' => [ 'number' ],
'maxlength' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
'min' => [ 'number' ],
'minlength' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
'pattern' => [ 'text', 'email', 'url', 'tel', 'number' ],
'spellcheck' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
'step' => [ 'number' ],
];
/**
* Get the allowed custom form control HTML attributes.
*
* @return array
*/
public static function get_allowed_html_attribute_names() {
/**
* Filter custom HTML attributes allowed on generated form controls.
*
* Prefix entries ending in `-*` match all attributes with that prefix.
* The renderer still blocks generated/runtime attributes such as
* `name`, `id`, `type`, `value`, `required`, `aria-required`,
* `aria-invalid`, `style`, event handlers, `form*`, and `data-gb-*`.
*
* @since 2.6.0
* @param array $allowed_attributes Allowed attribute names and prefix markers.
*/
$allowed_attributes = apply_filters(
'generateblocks_form_control_html_attributes',
self::$allowed_html_attributes
);
if ( ! is_array( $allowed_attributes ) ) {
return self::$allowed_html_attributes;
}
$normalized = [];
foreach ( $allowed_attributes as $attribute_name ) {
$attribute_name = self::normalize_html_attribute_name( $attribute_name, true );
if ( '' !== $attribute_name ) {
$normalized[] = $attribute_name;
}
}
return array_values( array_unique( $normalized ) );
}
/**
* Sanitize custom HTML attributes before rendering generated controls.
*
* @param mixed $html_attributes Raw htmlAttributes block attribute.
* @param string $field_type Normalized field type.
* @return array
*/
public static function sanitize_html_attributes( $html_attributes, $field_type = '' ) {
$field_type = is_string( $field_type ) ? $field_type : '';
if (
! is_array( $html_attributes ) ||
! in_array( $field_type, self::$custom_html_attribute_field_types, true )
) {
return [];
}
$sanitized = [];
$allowed_attributes = self::get_allowed_html_attribute_names();
foreach ( $html_attributes as $attribute_name => $attribute_value ) {
$attribute_name = self::normalize_html_attribute_name( $attribute_name );
if ( ! self::is_allowed_html_attribute( $attribute_name, $field_type, $allowed_attributes ) ) {
continue;
}
if ( is_array( $attribute_value ) || is_object( $attribute_value ) || null === $attribute_value ) {
continue;
}
$attribute_value = (string) $attribute_value;
if ( '' === $attribute_value && 0 !== strpos( $attribute_name, 'data-' ) ) {
continue;
}
$sanitized[ $attribute_name ] = $attribute_value;
}
return $sanitized;
}
/**
* Normalize an HTML attribute name for allow-list checks.
*
* @param mixed $attribute_name Raw attribute name.
* @param bool $allow_prefix Whether `aria-*` style prefix markers are valid.
* @return string
*/
private static function normalize_html_attribute_name( $attribute_name, $allow_prefix = false ) {
if ( ! is_scalar( $attribute_name ) ) {
return '';
}
$attribute_name = strtolower( trim( (string) $attribute_name ) );
$is_prefix = $allow_prefix && '-*' === substr( $attribute_name, -2 );
$attribute_name = preg_replace( '/[^a-z0-9._:-]/', '', $attribute_name );
if ( $is_prefix ) {
if ( ! is_string( $attribute_name ) || ! preg_match( '/^[a-z][a-z0-9._:-]*-$/', $attribute_name ) ) {
return '';
}
return $attribute_name . '*';
}
if ( ! is_string( $attribute_name ) || ! preg_match( '/^[a-z][a-z0-9._:-]*$/', $attribute_name ) ) {
return '';
}
return $attribute_name;
}
/**
* Whether an attribute name can be rendered for a field type.
*
* @param string $attribute_name Normalized attribute name.
* @param string $field_type Normalized field type.
* @param array $allowed_attributes Allowed attribute names and prefix markers.
* @return bool
*/
private static function is_allowed_html_attribute( $attribute_name, $field_type, $allowed_attributes ) {
if ( '' === $attribute_name ) {
return false;
}
if (
in_array( $attribute_name, self::$reserved_html_attributes, true ) ||
0 === strpos( $attribute_name, 'on' ) ||
0 === strpos( $attribute_name, 'form' ) ||
0 === strpos( $attribute_name, 'data-gb-' )
) {
return false;
}
$is_allowed = false;
foreach ( $allowed_attributes as $allowed_attribute ) {
if ( '-*' === substr( $allowed_attribute, -2 ) ) {
$prefix = substr( $allowed_attribute, 0, -1 );
if ( 0 === strpos( $attribute_name, $prefix ) ) {
$is_allowed = true;
break;
}
continue;
}
if ( $attribute_name === $allowed_attribute ) {
$is_allowed = true;
break;
}
}
if ( ! $is_allowed ) {
return false;
}
if (
isset( self::$html_attribute_field_type_support[ $attribute_name ] ) &&
! in_array( $field_type, self::$html_attribute_field_type_support[ $attribute_name ], true )
) {
return false;
}
return true;
}
/**
* Get sanitized custom HTML attributes for a control block.
*
* @param array $attributes Block attributes.
* @param string $field_type Normalized field type.
* @return array
*/
private static function get_custom_html_attributes( $attributes, $field_type ) {
return self::sanitize_html_attributes( $attributes['htmlAttributes'] ?? [], $field_type );
}
/**
* Render the Form Field Control block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string
*/
public static function render_block( $attributes, $block_content, $block ) {
unset( $block_content );
$field_type = GenerateBlocks_Block_Form_Field::normalize_field_type(
GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldType', '' )
);
$field_name = sanitize_key(
GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldName', '' )
);
$unique_id = (string) GenerateBlocks_Block_Form_Field::get_context_value( $block, 'uniqueId', '' );
$required = 'hidden' !== $field_type && ! empty( GenerateBlocks_Block_Form_Field::get_context_value( $block, 'isRequired', false ) );
$field_id = GenerateBlocks_Block_Form_Field::get_field_id( $unique_id, $field_name );
$html = self::build_control( $attributes, $field_type, $field_name, $field_id, $required, $block );
return generateblocks_maybe_add_block_css(
$html,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
}
/**
* Build the control markup.
*
* @param array $attributes Block attributes.
* @param string $field_type Field type.
* @param string $field_name Field name.
* @param string $field_id Field ID.
* @param bool $required Whether required.
* @param mixed $block Block instance.
* @return string
*/
private static function build_control( $attributes, $field_type, $field_name, $field_id, $required, $block ) {
$default_value = GenerateBlocks_Block_Form_Field::resolve_default_value( $attributes['defaultValue'] ?? '', $block );
switch ( $field_type ) {
case 'textarea':
return self::build_textarea( $attributes, $field_name, $field_id, $required, $default_value );
case 'select':
return self::build_select( $attributes, $field_name, $field_id, $required, $default_value );
case 'checkbox':
return self::build_checkbox( $attributes, $field_name, $field_id, $required );
case 'radio':
return self::build_choice_group( $attributes, $field_name, $field_id, true, $required, $default_value );
case 'checkbox-group':
return self::build_choice_group( $attributes, $field_name, $field_id, false, false, '' );
default:
return self::build_input( $attributes, $field_name, $field_id, $field_type, $required, $default_value );
}
}
/**
* Build base control classes.
*
* @param array $attributes Block attributes.
* @param string $element Element class.
* @return string
*/
private static function get_control_classes( $attributes, $element ) {
return GenerateBlocks_Block_Form_Field::get_block_classes(
'gb-form-field-control',
$attributes,
[ $element ]
);
}
/**
* Build an input element.
*
* @param array $attributes Block attributes.
* @param string $name Field name.
* @param string $id Field ID.
* @param string $type Input type.
* @param bool $required Whether required.
* @param string $default_value Default value.
* @return string
*/
private static function build_input( $attributes, $name, $id, $type, $required, $default_value ) {
$attrs = array_merge(
self::get_custom_html_attributes( $attributes, $type ),
[
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
'type' => $type,
'name' => $name,
'id' => $id,
]
);
if ( ! empty( $attributes['placeholder'] ) && 'hidden' !== $type ) {
$attrs['placeholder'] = (string) $attributes['placeholder'];
}
if ( '' !== $default_value ) {
$attrs['value'] = $default_value;
}
if ( $required ) {
$attrs['required'] = true;
$attrs['aria-required'] = 'true';
}
return self::single_tag( 'input', $attrs );
}
/**
* Build a textarea element.
*
* @param array $attributes Block attributes.
* @param string $name Field name.
* @param string $id Field ID.
* @param bool $required Whether required.
* @param string $default_value Default value.
* @return string
*/
private static function build_textarea( $attributes, $name, $id, $required, $default_value ) {
$attrs = array_merge(
self::get_custom_html_attributes( $attributes, 'textarea' ),
[
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
'name' => $name,
'id' => $id,
]
);
$rows = isset( $attributes['rows'] ) ? min( self::MAX_TEXTAREA_ROWS, absint( $attributes['rows'] ) ) : 0;
if ( $rows ) {
$attrs['rows'] = $rows;
}
if ( ! empty( $attributes['placeholder'] ) ) {
$attrs['placeholder'] = (string) $attributes['placeholder'];
}
if ( $required ) {
$attrs['required'] = true;
$attrs['aria-required'] = 'true';
}
return self::wrap_tag( 'textarea', $attrs, esc_textarea( $default_value ) );
}
/**
* Build a select element.
*
* @param array $attributes Block attributes.
* @param string $name Field name.
* @param string $id Field ID.
* @param bool $required Whether required.
* @param string $default_value Default selected value.
* @return string
*/
private static function build_select( $attributes, $name, $id, $required, $default_value ) {
$attrs = array_merge(
self::get_custom_html_attributes( $attributes, 'select' ),
[
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
'name' => $name,
'id' => $id,
]
);
if ( $required ) {
$attrs['required'] = true;
$attrs['aria-required'] = 'true';
}
$options_html = '';
$placeholder = $attributes['placeholder'] ?? '';
if ( '' !== $placeholder ) {
$options_html .= sprintf(
'<option value="" disabled%s>%s</option>',
'' === $default_value ? ' selected' : '',
esc_html( $placeholder )
);
}
foreach ( (array) ( $attributes['options'] ?? [] ) as $option ) {
$option = GenerateBlocks_Block_Form_Field::normalize_option( $option );
if ( ! $option ) {
continue;
}
$options_html .= sprintf(
'<option value="%s"%s>%s</option>',
esc_attr( $option['value'] ),
'' !== $default_value && $option['value'] === $default_value ? ' selected' : '',
esc_html( $option['label'] )
);
}
return self::wrap_tag( 'select', $attrs, $options_html );
}
/**
* Build a checkbox element.
*
* @param array $attributes Block attributes.
* @param string $name Field name.
* @param string $id Field ID.
* @param bool $required Whether required.
* @return string
*/
private static function build_checkbox( $attributes, $name, $id, $required ) {
$checked_value = ! empty( $attributes['checkedValue'] )
? sanitize_text_field( (string) $attributes['checkedValue'] )
: 'yes';
$attrs = array_merge(
self::get_custom_html_attributes( $attributes, 'checkbox' ),
[
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
'type' => 'checkbox',
'name' => $name,
'id' => $id,
'value' => $checked_value,
]
);
if ( $required ) {
$attrs['required'] = true;
$attrs['aria-required'] = 'true';
}
return self::single_tag( 'input', $attrs );
}
/**
* Build radio/checkbox-group options.
*
* @param array $attributes Block attributes.
* @param string $name Field name.
* @param string $id Field ID prefix.
* @param bool $radio Whether radio buttons.
* @param bool $required Whether radio group is required.
* @param string $default_value Default selected value.
* @return string
*/
private static function build_choice_group( $attributes, $name, $id, $radio, $required, $default_value ) {
$attrs = [
'class' => self::get_control_classes( $attributes, 'gb-form-field__group' ),
];
$html = '';
foreach ( (array) ( $attributes['options'] ?? [] ) as $index => $option ) {
$option = GenerateBlocks_Block_Form_Field::normalize_option( $option );
if ( ! $option ) {
continue;
}
$option_id = $id . '-' . $index;
$html .= sprintf(
'<label class="gb-form-field__group-option" for="%s">',
esc_attr( $option_id )
);
$input_attrs = [
'type' => $radio ? 'radio' : 'checkbox',
'name' => $radio ? $name : $name . '[]',
'id' => $option_id,
'value' => $option['value'],
];
if ( $radio && '' !== $default_value && $option['value'] === $default_value ) {
$input_attrs['checked'] = true;
}
if ( $radio && $required ) {
$input_attrs['required'] = true;
}
$html .= '<input ' . GenerateBlocks_Block_Form_Field::format_attributes( $input_attrs ) . ' />';
$html .= '<span>' . esc_html( $option['label'] ) . '</span>';
$html .= '</label>';
}
return self::wrap_tag( 'div', $attrs, $html );
}
/**
* Build a single tag.
*
* @param string $tag Tag name.
* @param array $attrs Generated attributes.
* @return string
*/
private static function single_tag( $tag, $attrs ) {
$attr_string = GenerateBlocks_Block_Form_Field::format_attributes( $attrs );
return sprintf( '<%s %s />', tag_escape( $tag ), $attr_string );
}
/**
* Build an element with content.
*
* @param string $tag Tag name.
* @param array $attrs Generated attributes.
* @param string $content Inner HTML.
* @return string
*/
private static function wrap_tag( $tag, $attrs, $content ) {
$attr_string = GenerateBlocks_Block_Form_Field::format_attributes( $attrs );
return sprintf(
'<%1$s %2$s>%3$s</%1$s>',
tag_escape( $tag ),
$attr_string,
$content
);
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Register the Form Field Control block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form-field-control.php';
@@ -0,0 +1,106 @@
<?php
/**
* Handles the Form Field Label block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form field label block class.
*/
class GenerateBlocks_Block_Form_Field_Label extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array
*/
protected static $block_ids = [];
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form-field-label';
/**
* Render the Form Field Label block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string
*/
public static function render_block( $attributes, $block_content, $block ) {
$field_type = GenerateBlocks_Block_Form_Field::normalize_field_type(
GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldType', '' )
);
if ( 'hidden' === $field_type ) {
return '';
}
$field_name = (string) GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldName', '' );
$unique_id = (string) GenerateBlocks_Block_Form_Field::get_context_value( $block, 'uniqueId', '' );
$required = ! empty( GenerateBlocks_Block_Form_Field::get_context_value( $block, 'isRequired', false ) );
$is_group = GenerateBlocks_Block_Form_Field::is_group_type( $field_type );
$has_content = '' !== trim( wp_strip_all_tags( (string) ( $attributes['content'] ?? '' ) ) );
if ( ! $has_content ) {
return '';
}
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
$processor = new WP_HTML_Tag_Processor( $block_content );
$tag_name = $is_group ? 'legend' : 'label';
if ( $processor->next_tag( $tag_name ) ) {
if ( ! $is_group ) {
$processor->set_attribute( 'for', GenerateBlocks_Block_Form_Field::get_field_id( $unique_id, $field_name ) );
}
$block_content = $processor->get_updated_html();
}
}
return $required ? self::append_required_marker( $block_content, $is_group ? 'legend' : 'label' ) : $block_content;
}
/**
* Append the runtime required marker inside the saved label/legend markup.
*
* @param string $html Label block HTML.
* @param string $tag_name Label tag name.
* @return string
*/
private static function append_required_marker( $html, $tag_name ) {
if ( false !== strpos( $html, 'gb-form-field__required' ) ) {
return $html;
}
$tag_name = tag_escape( $tag_name );
$updated = preg_replace(
sprintf( '#</%s>\s*$#i', preg_quote( $tag_name, '#' ) ),
'<span class="gb-form-field__required" aria-hidden="true"> *</span></' . $tag_name . '>',
$html,
1
);
return is_string( $updated ) ? $updated : $html;
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Register the Form Field Label block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form-field-label.php';
@@ -0,0 +1,395 @@
<?php
/**
* Handles the Form Field wrapper block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form field wrapper block class.
*/
class GenerateBlocks_Block_Form_Field extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array
*/
protected static $block_ids = [];
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form-field';
/**
* All allowed field types.
*
* @var array
*/
private static $allowed_types = [ 'text', 'email', 'url', 'tel', 'number', 'hidden', 'textarea', 'select', 'checkbox', 'radio', 'checkbox-group' ];
/**
* Field types that support same-value match validation.
*
* @var array
*/
private static $matchable_types = [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ];
/**
* Allowed display condition operators.
*
* @var array
*/
private static $allowed_condition_operators = [ 'is', 'isnot', 'isempty', 'isnotempty' ];
/**
* Per-render instance number, set by GenerateBlocks_Pro_Form_Render before
* rendering each form.
*
* @var int
*/
private static $render_instance = 0;
/**
* Set the current render instance number.
*
* @param int $instance Instance number (1+) or 0 to reset.
*/
public static function set_render_instance( $instance ) {
self::$render_instance = (int) $instance;
}
/**
* Get the current render instance number.
*
* @return int
*/
public static function get_render_instance() {
return (int) self::$render_instance;
}
/**
* Suffix appended to every field ID for the current render.
*
* @return string
*/
public static function id_suffix() {
return self::$render_instance > 0 ? '-' . self::$render_instance : '';
}
/**
* Normalize a field type.
*
* @param mixed $field_type Raw field type.
* @return string
*/
public static function normalize_field_type( $field_type ) {
$field_type = is_string( $field_type ) ? $field_type : '';
if ( '' === $field_type ) {
return 'text';
}
if ( ! in_array( $field_type, self::$allowed_types, true ) ) {
return 'text';
}
return $field_type;
}
/**
* Whether a field type uses the wrapper as a fieldset.
*
* @param string $field_type Field type.
* @return bool
*/
public static function is_group_type( $field_type ) {
return in_array( self::normalize_field_type( $field_type ), [ 'radio', 'checkbox-group' ], true );
}
/**
* Get a field ID shared by label/control children.
*
* @param string $unique_id Wrapper unique ID.
* @param string $field_name Field name fallback.
* @return string
*/
public static function get_field_id( $unique_id, $field_name = '' ) {
$key = $unique_id ? $unique_id : sanitize_key( $field_name );
if ( '' === $key ) {
$key = 'field';
}
return 'gb-field-' . sanitize_html_class( $key ) . self::id_suffix();
}
/**
* Get a value from a WP_Block context object.
*
* @param mixed $block Block object.
* @param string $key Context key suffix.
* @param mixed $fallback Fallback.
* @return mixed
*/
public static function get_context_value( $block, $key, $fallback = '' ) {
$context_key = 'generateblocks-pro/form-field/' . $key;
if ( is_object( $block ) && isset( $block->context ) && is_array( $block->context ) && array_key_exists( $context_key, $block->context ) ) {
return $block->context[ $context_key ];
}
return $fallback;
}
/**
* Build classes for a styles-builder-aware block.
*
* @param string $base_class Base class.
* @param array $attributes Block attributes.
* @param array $extra Extra classes.
* @return string
*/
public static function get_block_classes( $base_class, $attributes, $extra = [] ) {
$classes = [ $base_class ];
if ( ! empty( $attributes['globalClasses'] ) && is_array( $attributes['globalClasses'] ) ) {
foreach ( $attributes['globalClasses'] as $class_name ) {
$class_name = sanitize_html_class( $class_name );
if ( $class_name ) {
$classes[] = $class_name;
}
}
}
if ( ! empty( $attributes['styles'] ) && is_array( $attributes['styles'] ) && ! empty( $attributes['uniqueId'] ) ) {
$classes[] = $base_class . '-' . sanitize_html_class( $attributes['uniqueId'] );
}
foreach ( $extra as $class_name ) {
$class_name = sanitize_html_class( $class_name );
if ( $class_name ) {
$classes[] = $class_name;
}
}
return implode( ' ', array_unique( array_filter( $classes ) ) );
}
/**
* Format an associative attribute array.
*
* @param array $attributes Attribute name/value pairs.
* @return string
*/
public static function format_attributes( $attributes ) {
$out = [];
foreach ( $attributes as $name => $value ) {
if ( true === $value ) {
$out[] = esc_attr( $name );
continue;
}
if ( false === $value || null === $value || is_array( $value ) || is_object( $value ) ) {
continue;
}
$out[] = sprintf( '%s="%s"', esc_attr( $name ), esc_attr( (string) $value ) );
}
return implode( ' ', $out );
}
/**
* Normalize a choice option for rendering.
*
* @param mixed $option Raw option attribute.
* @return array|null Normalized label/value pair or null for empty rows.
*/
public static function normalize_option( $option ) {
if ( ! is_array( $option ) ) {
return null;
}
$label = isset( $option['label'] ) ? (string) $option['label'] : '';
$value = isset( $option['value'] ) ? (string) $option['value'] : '';
if ( '' === $value && '' !== $label ) {
$value = $label;
}
if ( '' === $label && '' !== $value ) {
$label = $value;
}
if ( '' === $label && '' === $value ) {
return null;
}
return [
'label' => $label,
'value' => $value,
];
}
/**
* Sanitize display conditions before rendering them as frontend data attrs.
*
* Incomplete rows can be left behind while editing, especially if a trigger
* field is removed before the empty row is deleted. Those must not create
* `data-gb-conditions`, because an empty field name evaluates as empty on the
* frontend and causes a hidden-then-revealed layout shift.
*
* @param mixed $conditions Raw conditions attribute.
* @return array
*/
private static function sanitize_conditions( $conditions ) {
if ( ! is_array( $conditions ) ) {
return [];
}
$sanitized = [];
foreach ( $conditions as $condition ) {
if ( ! is_array( $condition ) ) {
continue;
}
$field = sanitize_key( $condition['field'] ?? '' );
$operator = sanitize_key( $condition['operator'] ?? '' );
if ( '' === $field || ! in_array( $operator, self::$allowed_condition_operators, true ) ) {
continue;
}
$sanitized[] = [
'field' => $field,
'operator' => $operator,
'value' => (string) ( $condition['value'] ?? '' ),
];
}
return $sanitized;
}
/**
* Resolve dynamic tags in a default value.
*
* @param string $default_value Raw default value.
* @param mixed $block Block instance.
* @return string
*/
public static function resolve_default_value( $default_value, $block ) {
$default_value = (string) $default_value;
if ( '' !== $default_value && false !== strpos( $default_value, '{{' ) && class_exists( 'GenerateBlocks_Register_Dynamic_Tag' ) ) {
$default_value = GenerateBlocks_Register_Dynamic_Tag::replace_tags(
$default_value,
[],
$block
);
}
return (string) $default_value;
}
/**
* Render the Form Field wrapper block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string The modified block content.
*/
public static function render_block( $attributes, $block_content, $block ) {
unset( $block );
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
$field_type = self::normalize_field_type( $attributes['fieldType'] ?? '' );
if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
return $block_content;
}
$processor = new WP_HTML_Tag_Processor( $block_content );
$tag_name = self::is_group_type( $field_type ) ? 'fieldset' : 'div';
if ( ! $processor->next_tag( $tag_name ) ) {
return $block_content;
}
$modifier_map = [
'checkbox' => 'gb-form-field--checkbox',
'radio' => 'gb-form-field--radio',
'checkbox-group' => 'gb-form-field--checkbox-group',
];
if ( isset( $modifier_map[ $field_type ] ) ) {
$processor->add_class( $modifier_map[ $field_type ] );
}
// The wrapper is a native <fieldset>, whose implicit "group" role does not
// support aria-required (Lighthouse/axe: aria-allowed-attr). The required
// state is carried by the native `required` attribute on the radio inputs;
// checkbox groups have no native per-input required, so they validate via
// data-gb-required + JS. Either way the <fieldset> needs no aria-required.
if ( 'checkbox-group' === $field_type && ! empty( $attributes['isRequired'] ) ) {
$processor->set_attribute( 'data-gb-required', 'true' );
}
$field_name = sanitize_key( $attributes['fieldName'] ?? '' );
$matches_field = sanitize_key( $attributes['matchesField'] ?? '' );
if (
$field_name &&
$matches_field &&
$field_name !== $matches_field &&
in_array( $field_type, self::$matchable_types, true )
) {
$processor->set_attribute( 'data-gb-field-name', $field_name );
$processor->set_attribute( 'data-gb-matches-field', $matches_field );
$processor->set_attribute(
'data-gb-match-message',
__( 'The matching fields must have the same value.', 'generateblocks-pro' )
);
$processor->set_attribute(
'data-gb-match-message-with-label',
/* translators: %1$s: source field label, %2$s: target field label. */
__( 'Please make sure "%1$s" matches "%2$s".', 'generateblocks-pro' )
);
$processor->set_attribute(
'data-gb-match-message-with-target',
/* translators: %s: target field label. */
__( 'This field must match "%s".', 'generateblocks-pro' )
);
}
$conditions = self::sanitize_conditions( $attributes['conditions'] ?? [] );
if ( ! empty( $conditions ) ) {
$processor->set_attribute( 'data-gb-conditions', wp_json_encode( $conditions ) );
$processor->set_attribute( 'hidden', true );
}
return $processor->get_updated_html();
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Form field block loader.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form-field.php';
@@ -0,0 +1,77 @@
<?php
/**
* Form Render block.
*
* Page-side reference. Carries `formId` (a `gblocks_form` post ID) plus
* styling attributes; everything else (fields, actions, email, Turnstile,
* success/error messages) lives on the form post itself.
*
* Render delegates to the shared render helper so editor preview and
* public output stay byte-for-byte equivalent.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form Render block class.
*/
class GenerateBlocks_Block_Form_Render extends GenerateBlocks_Block {
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form-render';
/**
* Render the Form Render block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string The modified block content.
*/
public static function render_block( $attributes, $block_content, $block ) {
unset( $block_content );
$form_id = absint( $attributes['formId'] ?? 0 );
if ( ! $form_id ) {
return '';
}
$host_post_id = $block->context['postId'] ?? get_the_ID();
$host_post_id = $host_post_id ? absint( $host_post_id ) : 0;
$rendered = GenerateBlocks_Pro_Form_Render::render( $form_id, 'public', [ 'host_post_id' => $host_post_id ] );
if ( is_wp_error( $rendered ) ) {
// Public render failure — emit nothing rather than leaking error
// detail. Admin users see a placeholder hint to help debugging.
if ( current_user_can( GenerateBlocks_Pro_Form_Post_Type::get_capability( 'manage' ) ) ) {
return sprintf(
'<div class="gb-form gb-form--missing">%s</div>',
esc_html__( 'Form not available. Make sure the referenced form is published.', 'generateblocks-pro' )
);
}
return '';
}
GenerateBlocks_Pro_Form_Render::enqueue_public_assets();
return $rendered;
}
/**
* Enqueue block assets.
*/
public static function enqueue_assets() {
GenerateBlocks_Pro_Form_Render::enqueue_public_assets();
}
}
@@ -0,0 +1,32 @@
<?php
/**
* Form Render block loader.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form-render.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_form_block_editor_settings', 20 );
/**
* Add block editor settings for the form blocks.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_form_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-form-render',
'.editor-styles-wrapper .wp-block-generateblocks-pro-form',
'.editor-styles-wrapper .wp-block-generateblocks-pro-form-field',
'.editor-styles-wrapper .wp-block-generateblocks-pro-form-field-label',
'.editor-styles-wrapper .wp-block-generateblocks-pro-form-field-control',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
@@ -0,0 +1,62 @@
<?php
/**
* Form block.
*
* Authoring container that lives inside `gblocks_form` post content. Renders
* its inner form-field blocks with our standard wrapper class. Runtime
* action/method/security attributes are added by class-form-render.php while
* a saved form post is being rendered.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form block class.
*/
class GenerateBlocks_Block_Form extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array
*/
protected static $block_ids = [];
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form';
/**
* Render the Form block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string The modified block content.
*/
public static function render_block( $attributes, $block_content, $block ) {
unset( $block );
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( class_exists( 'GenerateBlocks_Pro_Form_Render' ) ) {
return GenerateBlocks_Pro_Form_Render::prepare_form_block_output( $block_content );
}
return $block_content;
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Form block loader.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form.php';
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Menu_Container extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/menu-container';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the menu container block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-menu-container.php';
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Menu_Toggle extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/menu-toggle';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,9 @@
<?php
/**
* Handle the menu toggle block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-menu-toggle.php';
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Navigation extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/navigation';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,62 @@
<?php
/**
* Handle the navigation block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-navigation.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_navigation_block_editor_settings', 20 );
/**
* Add block editor settings for the navigation block.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_navigation_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-classic-menu',
'.editor-styles-wrapper .wp-block-generateblocks-pro-menu-container',
'.editor-styles-wrapper .wp-block-generateblocks-pro-menu-toggle',
'.editor-styles-wrapper .wp-block-generateblocks-pro-navigation',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
add_filter( 'generateblocks_block_css', 'generateblocks_pro_navigation_block_css', 10, 2 );
/**
* Add block CSS for the navigation block.
*
* @param string $css The CSS to add.
* @param array $block The block data.
*
* @return string
*/
function generateblocks_pro_navigation_block_css( $css, $block ) {
if ( ! isset( $block['attributes']['uniqueId'] ) ) {
return $css;
}
$block_name = $block['block_name'] ?? '';
if ( 'generateblocks-pro/navigation' !== $block_name ) {
return $css;
}
$selector = '.gb-navigation-' . $block['attributes']['uniqueId'];
$mobile_breakpoint = $block['attributes']['htmlAttributes']['data-gb-mobile-breakpoint'] ?? '';
if ( $mobile_breakpoint ) {
$css .= "@media (width > {$mobile_breakpoint}) {{$selector} .gb-menu-toggle {display: none;}}";
$css .= "@media (max-width: {$mobile_breakpoint}) {{$selector} .gb-menu-container:not(.gb-menu-container--toggled) {display: none;}}";
} else {
$css .= "{$selector} .gb-menu-toggle {display: none;}";
}
return $css;
}
@@ -0,0 +1,70 @@
<?php
/**
* Handles the Content block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Content block class.
*/
class GenerateBlocks_Block_Site_Header extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/site-header';
/**
* Store when to enqueue assets.
*
* @var array $assets_to_enqueue The assets to enqueue.
*/
public static $assets_to_enqueue = [];
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
$html_attributes = $attributes['htmlAttributes'] ?? [];
$is_sticky = array_key_exists( 'data-gb-is-sticky', $html_attributes );
if ( $is_sticky ) {
wp_enqueue_script(
'generateblocks-pro-sticky',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/sticky-element.js',
[],
GENERATEBLOCKS_PRO_VERSION,
true
);
}
return $block_content;
}
}
@@ -0,0 +1,25 @@
<?php
/**
* Handle the navigation block.
*
* @package GenerateBlocks Pro
*/
// Include our files.
require_once 'class-site-header.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_site_header_block_editor_settings', 20 );
/**
* Add block editor settings for the navigation block.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_site_header_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-site-header',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
@@ -0,0 +1,61 @@
<?php
/**
* Handles the Tab Item block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tabs menu block class.
*/
class GenerateBlocks_Block_Tab_Item extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/tab-item';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( $attributes['tabItemOpen'] ) {
$processor = class_exists( 'WP_HTML_Tag_Processor' )
? new WP_HTML_Tag_Processor( $block_content )
: false;
if ( $processor && $processor->next_tag( $attributes['tagName'] ?? 'div' ) ) {
$processor->add_class( 'gb-tabs__item-open' );
$block_content = $processor->get_updated_html();
}
}
return $block_content;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Tab Items block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tabs menu block class.
*/
class GenerateBlocks_Block_Tab_Items extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/tab-items';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,82 @@
<?php
/**
* Handles the Tab Menu Item block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tabs menu block class.
*/
class GenerateBlocks_Block_Tab_Menu_Item extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/tab-menu-item';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
return $block_content;
}
$processor = new WP_HTML_Tag_Processor( $block_content );
$tag_name = $attributes['tagName'] ?? 'div';
$main_tag = $processor->next_tag( $tag_name );
$updated_html = false;
if ( $processor && $main_tag ) {
if ( $attributes['tabItemOpen'] ) {
$processor->add_class( 'gb-block-is-current' );
$updated_html = true;
}
if ( 'div' === $tag_name ) {
if ( ! $processor->get_attribute( 'tabindex' ) ) {
$processor->set_attribute( 'tabindex', '0' );
}
if ( ! $processor->get_attribute( 'role' ) ) {
$processor->set_attribute( 'role', 'button' );
}
$updated_html = true;
}
if ( $updated_html ) {
$block_content = $processor->get_updated_html();
}
}
return $block_content;
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Handles the Tabs Menu block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tabs menu block class.
*/
class GenerateBlocks_Block_Tabs_Menu extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/tabs-menu';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
return $block_content;
}
}
@@ -0,0 +1,91 @@
<?php
/**
* Handles the Tabs block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Tabs block class.
*/
class GenerateBlocks_Block_Tabs extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array $block_ids The current block id.
*/
protected static $block_ids = [];
/**
* Store our block name.
*
* @var string $block_name The block name.
*/
public static $block_name = 'generateblocks-pro/tabs';
/**
* Render the Element block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param array $block The block.
*/
public static function render_block( $attributes, $block_content, $block ) {
// Add styles to this block if needed.
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
if ( ! wp_style_is( 'generateblocks-tabs', 'enqueued' ) ) {
self::enqueue_style();
}
if ( ! wp_script_is( 'generateblocks-tabs', 'enqueued' ) ) {
self::enqueue_assets();
}
return $block_content;
}
/**
* Enqueue block styles.
*/
private static function enqueue_style() {
wp_enqueue_style(
'generateblocks-tabs',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/blocks/tabs/tabs.css',
[],
GENERATEBLOCKS_PRO_VERSION
);
}
/**
* Enqueue block scripts.
*/
private static function enqueue_scripts() {
wp_enqueue_script(
'generateblocks-tabs',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/tabs.js',
[],
GENERATEBLOCKS_PRO_VERSION,
true
);
}
/**
* Enqueue block assets.
*/
public static function enqueue_assets() {
self::enqueue_scripts();
self::enqueue_style();
}
}
@@ -0,0 +1,36 @@
<?php
/**
* Handles the accordion block.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
require_once 'class-tabs.php';
require_once 'class-tabs-menu.php';
require_once 'class-tab-menu-item.php';
require_once 'class-tab-items.php';
require_once 'class-tab-item.php';
add_filter( 'block_editor_settings_all', 'generateblocks_pro_tabs_block_editor_settings', 20 );
/**
* Add block editor settings for the tabs block.
*
* @param array $settings The block editor settings.
*/
function generateblocks_pro_tabs_block_editor_settings( $settings ) {
$blocks_to_reset = [
'.editor-styles-wrapper .wp-block-generateblocks-pro-tabs',
'.editor-styles-wrapper .wp-block-generateblocks-pro-tabs-menu',
'.editor-styles-wrapper .wp-block-generateblocks-pro-tab-menu-item',
'.editor-styles-wrapper .wp-block-generateblocks-pro-tab-items',
'.editor-styles-wrapper .wp-block-generateblocks-pro-tab-item',
];
$css = implode( ',', $blocks_to_reset ) . ' {max-width:unset;margin:0}';
$settings['styles'][] = [ 'css' => $css ];
return $settings;
}
@@ -0,0 +1,175 @@
<?php
/**
* General actions and filters.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Build our settings area.
*
* @since 1.0
*/
class GenerateBlocks_Asset_Library {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initiate our class.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
add_action( 'generateblocks_dashboard_tabs', array( $this, 'add_tab' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'generateblocks_dashboard_screens', array( $this, 'add_to_dashboard_pages' ) );
add_action( 'generateblocks_asset_library_area', array( $this, 'add_shape_library' ), 5 );
add_action( 'generateblocks_asset_library_area', array( $this, 'add_icon_library' ), 5 );
}
/**
* Add our Dashboard menu item.
*/
public function add_menu() {
$settings = add_submenu_page(
'generateblocks',
__( 'Asset Library', 'generateblocks-pro' ),
__( 'Asset Library', 'generateblocks-pro' ),
'manage_options',
'generateblocks-asset-library',
array( $this, 'asset_library' ),
5
);
}
/**
* Add a Local Templates tab to the GB Dashboard tabs.
*
* @param array $tabs The existing tabs.
*/
public function add_tab( $tabs ) {
$screen = get_current_screen();
$tabs['asset-library'] = array(
'name' => __( 'Asset Library', 'generateblocks-pro' ),
'url' => admin_url( 'admin.php?page=generateblocks-asset-library' ),
'class' => 'generateblocks_page_generateblocks-asset-library' === $screen->id ? 'active' : '',
);
return $tabs;
}
/**
* Enqueue our scripts.
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ( 'generateblocks_page_generateblocks-asset-library' === $screen->id ) {
wp_enqueue_media();
$assets = generateblocks_pro_get_enqueue_assets(
'asset-library',
[
'dependencies' => array( 'wp-api', 'wp-i18n', 'wp-components', 'wp-element', 'wp-api-fetch' ),
'version' => GENERATEBLOCKS_PRO_VERSION,
]
);
wp_enqueue_script(
'generateblocks-pro-asset-library',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/asset-library.js',
$assets['dependencies'],
$assets['version'],
true
);
wp_set_script_translations( 'generateblocks-pro-asset-library', 'generateblocks-pro', GENERATEBLOCKS_PRO_DIR . 'languages' );
wp_localize_script(
'generateblocks-pro-asset-library',
'generateBlocksProSettings',
array(
'shapes' => get_option( 'generateblocks_svg_shapes', array() ),
'icons' => get_option( 'generateblocks_svg_icons', array() ),
'hasSVGSupport' => generateblocks_pro_has_svg_support(),
)
);
wp_enqueue_style(
'generateblocks-pro-asset-library',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/asset-library.css',
array( 'wp-components' ),
GENERATEBLOCKS_PRO_VERSION
);
}
}
/**
* Add to our Dashboard pages.
*
* @since 1.0.0
* @param array $pages The existing pages.
*/
public function add_to_dashboard_pages( $pages ) {
$pages[] = 'generateblocks_page_generateblocks-asset-library';
return $pages;
}
/**
* Output our Dashboard HTML.
*
* @since 1.0.0
*/
public function asset_library() {
?>
<div class="wrap gblocks-dashboard-wrap">
<div class="generateblocks-settings-area generateblocks-asset-library-area">
<?php do_action( 'generateblocks_asset_library_area' ); ?>
</div>
</div>
<?php
}
/**
* Add Shape Library container.
*
* @since 1.0.0
*/
public function add_shape_library() {
echo '<div id="gblocks-shape-library"></div>';
}
/**
* Add Icon Library container.
*
* @since 1.0.0
*/
public function add_icon_library() {
echo '<div id="gblocks-icon-library"></div>';
}
}
GenerateBlocks_Asset_Library::get_instance();
@@ -0,0 +1,303 @@
<?php
/**
* Handle post types in GenerateBlocks Pro.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The Local templates class.
*/
class GenerateBlocks_Pro_Global_Styles {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initiate class.
*/
public function __construct() {
// Disable this deprecated feature if no posts exist.
// We run this if `is_admin()` as we don't want the query on the frontend.
if ( is_admin() ) {
$global_styles = get_posts(
[
'post_type' => 'gblocks_global_style',
'post_status' => [ 'any', 'trash' ],
'posts_per_page' => 1,
'fields' => 'ids',
]
);
if ( 0 === count( (array) $global_styles ) ) {
add_option( 'generateblocks_global_styles', array(), '', true );
$viewing_post_type = isset( $_GET['post_type'] ) && 'gblocks_global_style' === sanitize_text_field( $_GET['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $viewing_post_type ) {
// Prevent an "invalid post type" error when deleting the last global style.
add_action( 'init', array( $this, 'add_custom_post_types' ), 100 );
}
return;
}
} else {
// If we have no Global Styles on the frontend, we can bail now.
$global_styles = get_option( 'generateblocks_global_styles', array() );
if ( empty( $global_styles ) ) {
return;
}
}
add_action( 'init', array( $this, 'add_custom_post_types' ), 100 );
add_action( 'save_post_gblocks_global_style', array( $this, 'build_css' ), 10, 2 );
add_filter( 'generateblocks_do_content', array( $this, 'do_blocks' ) );
add_filter( 'generateblocks_headline_selector_tagname', array( $this, 'change_headline_selector' ), 10, 2 );
add_filter( 'generateblocks_dashboard_screens', array( $this, 'add_to_dashboard_pages' ) );
add_filter( 'views_edit-gblocks_global_style', array( $this, 'deprecation_notice' ) );
}
/**
* Register custom post type.
*/
public function add_custom_post_types() {
register_post_type(
'gblocks_global_style',
array(
'labels' => array(
'name' => _x( 'Global Styles (Legacy)', 'Post Type General Name', 'generateblocks-pro' ),
'singular_name' => _x( 'Global Style', 'Post Type Singular Name', 'generateblocks-pro' ),
'menu_name' => __( 'Global Styles', 'generateblocks-pro' ),
'parent_item_colon' => __( 'Parent Global Style', 'generateblocks-pro' ),
'all_items' => __( 'Global Styles', 'generateblocks-pro' ),
'view_item' => __( 'View Global Style', 'generateblocks-pro' ),
'add_new_item' => __( 'Add New Global Style', 'generateblocks-pro' ),
'add_new' => __( 'Add New', 'generateblocks-pro' ),
'edit_item' => __( 'Edit Global Style', 'generateblocks-pro' ),
'update_item' => __( 'Update Global Style', 'generateblocks-pro' ),
'search_items' => __( 'Search Global Style', 'generateblocks-pro' ),
'not_found' => __( 'Not Found', 'generateblocks-pro' ),
'not_found_in_trash' => __( 'Not found in Trash', 'generateblocks-pro' ),
),
'public' => false,
'publicly_queryable' => false,
'has_archive' => false,
'show_ui' => true,
'exclude_from_search' => true,
'show_in_nav_menus' => false,
'rewrite' => false,
'hierarchical' => false,
'show_in_menu' => false,
'show_in_admin_bar' => true,
'show_in_rest' => true,
'capabilities' => array(
'create_posts' => false,
'publish_posts' => 'manage_options',
'edit_posts' => 'manage_options',
'edit_others_posts' => 'manage_options',
'delete_posts' => 'manage_options',
'delete_others_posts' => 'manage_options',
'read_private_posts' => 'manage_options',
'edit_post' => 'manage_options',
'delete_post' => 'manage_options',
'read_post' => 'manage_options',
),
'supports' => array(
'title',
'editor',
'thumbnail',
),
)
);
}
/**
* Add to our Dashboard pages.
*
* @since 1.0.0
* @param array $pages The existing pages.
*/
public function add_to_dashboard_pages( $pages ) {
$pages[] = 'edit-gblocks_global_style';
return $pages;
}
/**
* Build our global CSS.
*
* @since 1.0.0
* @param int $post_id The post ID.
* @param object $post The post.
*/
public function build_css( $post_id, $post ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$global_styles = get_option( 'generateblocks_global_styles', array() );
$global_style_attrs = get_option( 'generateblocks_global_style_attrs', array() );
// Set our content for this global style page.
$global_styles[ $post_id ]['content'] = $post->post_content;
$parsed_content = parse_blocks( $post->post_content );
$block_data = generateblocks_get_block_data( $parsed_content );
$defaults = generateblocks_get_block_defaults();
// Empty our global style IDs so they can be re-populated.
$global_style_attrs[ $post_id ]['ids'] = array();
// Remove IDs from our front-end data as it's not needed anymore.
if ( isset( $global_styles[ $post_id ]['ids'] ) ) {
unset( $global_styles[ $post_id ]['ids'] );
}
// Save all of our attribute values so we can use them in the editor.
foreach ( (array) $block_data as $name => $data ) {
if ( ! empty( $data ) && is_array( $data ) ) {
foreach ( $data as $attributes ) {
$defaultBlockName = 'container';
if ( 'button' === $name ) {
$defaultBlockName = 'button';
} elseif ( 'button-container' === $name ) {
$defaultBlockName = 'buttonContainer';
} elseif ( 'headline' === $name ) {
$defaultBlockName = 'headline';
} elseif ( 'grid' === $name ) {
$defaultBlockName = 'gridContainer';
}
// Save block defaults as values.
foreach ( $defaults[ $defaultBlockName ] as $key => $value ) {
if ( ! array_key_exists( $key, $attributes ) && ( ! empty( $value ) || 0 === $value ) ) {
$attributes[ $key ] = $value;
}
}
if ( ! isset( $global_style_attrs[ $post_id ]['ids'][ $name ] ) ) {
$global_style_attrs[ $post_id ]['ids'][ $name ][ $attributes['uniqueId'] ] = array();
}
if ( isset( $attributes['uniqueId'] ) ) {
$global_style_attrs[ $post_id ]['ids'][ $name ][ $attributes['uniqueId'] ] = $attributes;
}
}
}
}
if ( 'publish' !== $post->post_status ) {
unset( $global_styles[ $post_id ] );
unset( $global_style_attrs[ $post_id ] );
}
// Force regenerate our static CSS files.
update_option( 'generateblocks_dynamic_css_posts', array() );
update_option( 'generateblocks_global_styles', $global_styles );
update_option( 'generateblocks_global_style_attrs', $global_style_attrs );
}
/**
* Tell GB about our styles.
*
* @param string $content The existing content.
*/
public function do_blocks( $content ) {
$global_styles = get_option( 'generateblocks_global_styles', array() );
foreach ( (array) $global_styles as $id => $data ) {
$content = $data['content'] . $content;
}
return $content;
}
/**
* Remove the tagname from our global style Headline selectors.
*
* @param boolean $do Whether to include the tagname or not.
* @param array $atts The attributes for the block.
*/
public function change_headline_selector( $do, $atts ) {
if ( isset( $atts['isGlobalStyle'] ) && $atts['isGlobalStyle'] ) {
$do = false;
}
return $do;
}
/**
* Show a deprecation notice in our post type.
*
* @param string $views Existing text in this filter.
*/
public function deprecation_notice( $views ) {
?>
<style>
.gb-deprecation-notice {
background: #fef8ee;
border-left: 5px solid #f0b849;
padding: 20px;
margin: 20px 0;
}
.gb-deprecation-notice p {
margin-top: 0;
}
.gb-deprecation-notice p:last-child {
margin-bottom: 0;
}
</style>
<div class="gb-deprecation-notice">
<p><?php esc_html_e( 'This Global Styles system has been deprecated. You can still edit and use existing styles, but you cannot add new ones.', 'generateblocks-pro' ); ?></p>
<p>
<?php
printf(
/* translators: %1$s and %2$s are placeholders for the opening and closing anchor tags */
esc_html__(
'Please use the new %1$sGlobal Styles%2$s system instead.',
'generateblocks-pro'
),
'<a href="' . esc_url( admin_url( 'admin.php?page=generateblocks-styles' ) ) . '">',
'</a>'
);
?>
</p>
<p>
<a href="https://docs.generateblocks.com/article/global-styles-overview/" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Learn more about the new Global Styles', 'generateblocks-pro' ); ?> <svg xmlns="http://www.w3.org/2000/svg" style="fill: currentColor;width: 15px;height:15px;position: relative;top: 3px;left: 3px;" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"></path></svg></a></p>
</div>
<?php
return $views;
}
}
GenerateBlocks_Pro_Global_Styles::get_instance();
@@ -0,0 +1,215 @@
<?php
/**
* Handle post types in GenerateBlocks Pro.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The Local templates class.
*/
class GenerateBlocks_Pro_Local_Templates {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initiate class.
*/
public function __construct() {
add_action( 'init', array( $this, 'add_custom_post_types' ) );
if ( ! is_admin() ) {
return;
}
$admin_settings = wp_parse_args(
get_option( 'generateblocks_admin', array() ),
generateblocks_pro_get_admin_option_defaults()
);
if ( ! $admin_settings['enable_local_templates'] ) {
return;
}
add_action( 'admin_init', array( $this, 'maybe_redirect_empty_legacy_templates' ) );
add_action( 'admin_head', array( $this, 'fix_menu' ) );
add_action( 'admin_footer', [ $this, 'add_scripts' ] );
}
/**
* Register custom post type.
*/
public function add_custom_post_types() {
register_post_type(
'gblocks_templates',
array(
'labels' => array(
'name' => _x( 'Local Patterns (Legacy)', 'Post Type General Name', 'generateblocks-pro' ),
'singular_name' => _x( 'Local Pattern', 'Post Type Singular Name', 'generateblocks-pro' ),
'menu_name' => __( 'Local Patterns', 'generateblocks-pro' ),
'parent_item_colon' => __( 'Parent Local Pattern', 'generateblocks-pro' ),
'all_items' => __( 'Local Patterns', 'generateblocks-pro' ),
'view_item' => __( 'View Local Pattern', 'generateblocks-pro' ),
'add_new_item' => __( 'Add New Local Pattern', 'generateblocks-pro' ),
'add_new' => __( 'Add New', 'generateblocks-pro' ),
'edit_item' => __( 'Edit Local Pattern', 'generateblocks-pro' ),
'update_item' => __( 'Update Local Pattern', 'generateblocks-pro' ),
'search_items' => __( 'Search Local Pattern', 'generateblocks-pro' ),
'not_found' => __( 'Not Found', 'generateblocks-pro' ),
'not_found_in_trash' => __( 'Not found in Trash', 'generateblocks-pro' ),
),
'public' => false,
'publicly_queryable' => false,
'has_archive' => false,
'show_ui' => true,
'exclude_from_search' => true,
'show_in_nav_menus' => false,
'rewrite' => false,
'hierarchical' => false,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'show_in_rest' => $this->should_show_in_rest(),
'capabilities' => array(
'create_posts' => 'do_not_allow',
'publish_posts' => 'manage_options',
'edit_posts' => 'manage_options',
'edit_others_posts' => 'manage_options',
'delete_posts' => 'manage_options',
'delete_others_posts' => 'manage_options',
'read_private_posts' => 'manage_options',
'edit_post' => 'manage_options',
'delete_post' => 'manage_options',
'read_post' => 'manage_options',
),
'supports' => array(
'title',
'editor',
'thumbnail',
),
)
);
}
/**
* Determine if the legacy CPT should be exposed through core REST routes.
*
* @return bool
*/
private function should_show_in_rest() {
if ( is_admin() ) {
return true;
}
if ( function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) {
return current_user_can( 'manage_options' );
}
return false;
}
/**
* Redirect the empty legacy CPT list back to the dashboard.
*/
public function maybe_redirect_empty_legacy_templates() {
global $pagenow;
if ( ! in_array( $pagenow, array( 'edit.php', 'post-new.php' ), true ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Redirecting a read-only admin URL.
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
if ( 'gblocks_templates' !== $post_type ) {
return;
}
if ( generateblocks_pro_has_legacy_patterns() ) {
return;
}
wp_safe_redirect( admin_url() );
exit;
}
/**
* Trick the WordPress menu to highlight our Patterns menu item
* when we're dealing with collections or categories.
*/
public function fix_menu() {
global $parent_file, $submenu_file, $post_type;
$screen = get_current_screen();
if ( 'edit-gblocks_templates' === $screen->id ) {
$parent_file = 'generateblocks'; // phpcs:ignore -- Override necessary.
$submenu_file = 'edit.php?post_type=wp_block'; // phpcs:ignore -- Override necessary.
}
}
/**
* Add a link to the new patterns.
*/
public function add_scripts() {
$screen = get_current_screen();
if ( 'edit-gblocks_templates' !== $screen->id ) {
return;
}
?>
<script>
document.addEventListener( 'DOMContentLoaded', () => {
const button = document.querySelector( '.page-title-action' );
const heading = document.querySelector( '.wrap .wp-heading-inline' );
const buttonParent = button ? button.parentNode : null;
const headingParent = heading ? heading.parentNode : null;
if ( ! buttonParent && ! headingParent ) {
return;
}
if ( button ) {
button.style.pointerEvents = 'none';
button.style.opacity = '0.5';
}
const newPatternsButton = document.createElement( 'a' );
newPatternsButton.classList.add( 'page-title-action' );
newPatternsButton.href = '<?php echo esc_url( admin_url( 'edit.php?post_type=wp_block' ) ); ?>';
newPatternsButton.textContent = '<?php echo esc_html( __( 'New Pattern Library', 'generateblocks-pro' ) ); ?>';
if ( buttonParent ) {
buttonParent.insertBefore( newPatternsButton, button );
return;
}
headingParent.insertBefore( newPatternsButton, heading.nextSibling );
} );
</script>
<?php
}
}
GenerateBlocks_Pro_Local_Templates::get_instance();
@@ -0,0 +1,79 @@
<?php
/**
* Handles option changes on plugin updates.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Process option updates if necessary.
*/
class GenerateBlocks_Pro_Plugin_Update {
/**
* Class instance.
*
* @access private
* @var $instance Class instance.
*/
private static $instance;
/**
* Initiator
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
if ( is_admin() ) {
add_action( 'admin_init', __CLASS__ . '::init', 5 );
} else {
add_action( 'wp', __CLASS__ . '::init', 5 );
}
}
/**
* Implement plugin update logic.
*
* @since 1.0.0
*/
public static function init() {
if ( is_customize_preview() ) {
return;
}
$saved_version = get_option( 'generateblocks_pro_version', false );
if ( false === $saved_version ) {
if ( 'admin_init' === current_action() ) {
// If we're in the admin, add our version to the database.
update_option( 'generateblocks_pro_version', sanitize_text_field( GENERATEBLOCKS_PRO_VERSION ) );
}
// Not an existing install, so no need to proceed further.
return;
}
if ( version_compare( $saved_version, GENERATEBLOCKS_PRO_VERSION, '=' ) ) {
return;
}
// Force regenerate our static CSS files.
update_option( 'generateblocks_dynamic_css_posts', array() );
// Last thing to do is update our version.
update_option( 'generateblocks_pro_version', sanitize_text_field( GENERATEBLOCKS_PRO_VERSION ) );
}
}
GenerateBlocks_Pro_Plugin_Update::get_instance();
@@ -0,0 +1,665 @@
<?php
/**
* Rest API functions
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class GenerateBlocks_Rest
*/
class GenerateBlocks_Pro_Rest extends WP_REST_Controller {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Namespace.
*
* @var string
*/
protected $namespace = 'generateblocks-pro/v';
/**
* Version.
*
* @var string
*/
protected $version = '1';
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* GenerateBlocks_Rest constructor.
*/
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* Register rest routes.
*/
public function register_routes() {
$namespace = $this->namespace . $this->version;
// Get Templates.
register_rest_route(
$namespace,
'/get_templates/',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_templates' ),
'permission_callback' => array( $this, 'edit_posts_permission' ),
)
);
// Get template data.
register_rest_route(
$namespace,
'/get_template_data/',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_template_data' ),
'permission_callback' => array( $this, 'edit_posts_permission' ),
)
);
// Save template library options.
register_rest_route(
$namespace,
'/template-library/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_template_library_settings' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
// Regenerate CSS Files.
register_rest_route(
$namespace,
'/sync_template_library/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'sync_template_library' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
// Update licensing.
register_rest_route(
$namespace,
'/license/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_licensing' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
// Update Shapes.
register_rest_route(
$namespace,
'/shape-settings/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_shape_settings' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
// Update Icons.
register_rest_route(
$namespace,
'/icon-settings/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_icon_settings' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
// Export Assets.
register_rest_route(
$namespace,
'/export-asset-group/',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'export_asset_group' ),
'permission_callback' => array( $this, 'update_settings_permission' ),
)
);
}
/**
* Get edit options permissions.
*
* @return bool
*/
public function update_settings_permission() {
return current_user_can( 'manage_options' );
}
/**
* Get edit posts permissions.
*
* @return bool
*/
public function edit_posts_permission() {
return current_user_can( 'edit_posts' );
}
/**
* Get templates.
*
* @return mixed
*/
public function get_templates() {
$url = 'https://library.generateblocks.com/wp-json/templates/get_templates';
$templates = get_transient( 'generateblocks_templates', false );
/*
* Get remote templates.
*/
if ( ! $templates ) {
$requested_templates = wp_remote_get( $url );
if ( ! is_wp_error( $requested_templates ) ) {
$new_templates = wp_remote_retrieve_body( $requested_templates );
$new_templates = json_decode( $new_templates, true );
if ( $new_templates && isset( $new_templates['response'] ) && is_array( $new_templates['response'] ) ) {
$templates = $new_templates['response'];
set_transient( 'generateblocks_templates', $templates, DAY_IN_SECONDS );
}
} else {
$templates = array(
array(
'error' => $requested_templates->get_error_messages(),
),
);
}
}
/*
* Get user templates from db.
*/
$args = array(
'post_type' => 'gblocks_templates',
'fields' => 'ids',
'no_found_rows' => true,
'post_status' => 'any',
'numberposts' => 500, // phpcs:ignore
'post_status' => 'publish',
);
$all_templates = get_posts( $args );
$local_templates = array();
foreach ( $all_templates as $id ) {
$image_id = get_post_thumbnail_id( $id );
$image_data = wp_get_attachment_image_src( $image_id, 'large' );
$local_templates[] = array(
'id' => $id,
'title' => get_the_title( $id ),
'types' => array(
array(
'slug' => 'local',
),
),
'url' => get_post_permalink( $id ),
'thumbnail' => isset( $image_data[0] ) ? $image_data[0] : false,
'thumbnail_width' => isset( $image_data[1] ) ? $image_data[1] : false,
'thumbnail_height' => isset( $image_data[2] ) ? $image_data[2] : false,
);
}
// merge all available templates.
$templates = array_merge( (array) $templates, $local_templates );
if ( is_array( $templates ) ) {
return $this->success( $templates );
} else {
return $this->error( 'no_templates', __( 'Templates not found.', 'generateblocks-pro' ) );
}
}
/**
* Get templates.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function get_template_data( WP_REST_Request $request ) {
$url = 'https://library.generateblocks.com/wp-json/templates/get_template';
$id = $request->get_param( 'id' );
$type = $request->get_param( 'type' );
$template_data = false;
switch ( $type ) {
case 'remote':
$cached_template_data = get_transient( 'generateblocks_template_data', array() );
if ( isset( $cached_template_data[ $id ] ) ) {
$template_data = $cached_template_data[ $id ];
}
if ( ! $template_data ) {
$requested_template_data = wp_remote_get(
add_query_arg(
array(
'id' => $id,
),
$url
)
);
if ( ! is_wp_error( $requested_template_data ) ) {
$new_template_data = wp_remote_retrieve_body( $requested_template_data );
$new_template_data = json_decode( $new_template_data, true );
if ( $new_template_data && isset( $new_template_data['response'] ) && is_array( $new_template_data['response'] ) ) {
$template_data = $new_template_data['response'];
$cached_template_data[ $id ] = $template_data;
set_transient( 'generateblocks_template_data', $cached_template_data, DAY_IN_SECONDS );
}
}
}
break;
case 'local':
$post = get_post( $id );
if ( $post && 'gblocks_templates' === $post->post_type ) {
$template_data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
);
}
break;
}
if ( is_array( $template_data ) ) {
return $this->success( $template_data );
} else {
return $this->error( 'no_template_data', __( 'Template data not found.', 'generateblocks-pro' ) );
}
}
/**
* Update Settings.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function update_template_library_settings( WP_REST_Request $request ) {
$current_settings = get_option( 'generateblocks_admin', array() );
$remote_templates = $request->get_param( 'enableRemoteTemplates' );
$local_templates = $request->get_param( 'enableLocalTemplates' );
$current_settings['enable_remote_templates'] = $remote_templates;
$current_settings['enable_local_templates'] = $local_templates;
update_option( 'generateblocks_admin', $current_settings );
return $this->success( __( 'Settings saved.', 'generateblocks-pro' ) );
}
/**
* Sync the template library.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function sync_template_library( WP_REST_Request $request ) {
delete_transient( 'generateblocks_templates' );
delete_transient( 'generateblocks_template_data' );
return $this->success( true );
}
/**
* Update Settings.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function update_licensing( WP_REST_Request $request ) {
$new_license_data = $request->get_param( 'licenseSettings' );
$new_settings = array();
$license_settings = wp_parse_args(
get_option( 'generateblocks_pro_licensing', array() ),
generateblocks_pro_get_license_defaults()
);
$old_license = $license_settings['key'];
$new_license = trim( $new_license_data['key'] );
if ( $new_license ) {
$api_params = array(
'edd_action' => 'activate_license',
'license' => sanitize_key( $new_license ),
'item_name' => rawurlencode( 'GenerateBlocks Pro' ),
'url' => home_url(),
);
} elseif ( $old_license && 'valid' === $license_settings['status'] ) {
$api_params = array(
'edd_action' => 'deactivate_license',
'license' => sanitize_key( $old_license ),
'item_name' => rawurlencode( 'GenerateBlocks Pro' ),
'url' => home_url(),
);
}
if ( isset( $api_params ) ) {
$response = wp_remote_post(
'https://generateblocks.com',
array(
'timeout' => 15,
'sslverify' => false,
'body' => $api_params,
)
);
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
if ( is_wp_error( $response ) ) {
return $this->failed( $response->get_error_message() );
} else {
$message = __( 'An error occurred, please try again.', 'generateblocks-pro' );
}
} else {
$license_data = json_decode( wp_remote_retrieve_body( $response ) );
if ( false === $license_data->success ) {
switch ( $license_data->error ) {
case 'expired':
$message = sprintf(
/* translators: License key expiration date. */
__( 'Your license key expired on %s.', 'generateblocks-pro' ),
date_i18n( get_option( 'date_format' ), strtotime( $license_data->expires, current_time( 'timestamp' ) ) ) // phpcs:ignore
);
break;
case 'disabled':
case 'revoked':
$message = __( 'Your license key has been disabled.', 'generateblocks-pro' );
break;
case 'missing':
$message = __( 'Invalid license.', 'generateblocks-pro' );
break;
case 'invalid':
case 'site_inactive':
$message = __( 'Your license is not active for this URL.', 'generateblocks-pro' );
break;
case 'item_name_mismatch':
/* translators: GenerateBlocks Pro */
$message = sprintf( __( 'This appears to be an invalid license key for %s.', 'generateblocks-pro' ), __( 'GenerateBlocks Pro', 'generateblocks-pro' ) );
break;
case 'no_activations_left':
$message = __( 'Your license key has reached its activation limit.', 'generateblocks-pro' );
break;
default:
$message = __( 'An error occurred, please try again.', 'generateblocks-pro' );
break;
}
}
}
$new_settings['status'] = esc_attr( $license_data->license );
}
if ( isset( $new_license_data['beta'] ) ) {
$new_settings['beta'] = sanitize_key( $new_license_data['beta'] );
}
$new_settings['key'] = sanitize_key( $new_license );
if ( is_array( $new_settings ) ) {
$current_settings = get_option( 'generateblocks_pro_licensing', array() );
update_option( 'generateblocks_pro_licensing', array_merge( $current_settings, $new_settings ) );
if ( ! isset( $api_params ) ) {
return $this->success( __( 'Settings saved.', 'generateblocks-pro' ) );
}
}
if ( ! empty( $message ) ) {
return $this->failed( $message );
}
return $this->success( $license_data );
}
/**
* Update Shapes.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function update_shape_settings( WP_REST_Request $request ) {
$current_settings = get_option( 'generateblocks_svg_shapes', array() );
$new_settings = $request->get_param( 'settings' );
$new_shapes = array();
$usedGroupNames = array();
foreach ( $new_settings as $index => $data ) {
// Build our group name and ID.
if ( ! in_array( $data['group'], $usedGroupNames ) ) {
$new_shapes[ $index ]['group'] = sanitize_text_field( $data['group'] );
$new_shapes[ $index ]['group_id'] = str_replace( ' ', '-', strtolower( $new_shapes[ $index ]['group'] ) );
$usedGroupNames[] = $new_shapes[ $index ]['group'];
} else {
$new_shapes[ $index ]['group'] = sanitize_text_field( $data['group'] . ' ' . $index );
$new_shapes[ $index ]['group_id'] = str_replace( ' ', '-', strtolower( $new_shapes[ $index ]['group'] ) );
$usedGroupNames[] = $new_shapes[ $index ]['group'];
}
if ( ! empty( $data['shapes'] ) ) {
foreach ( $data['shapes'] as $shape_index => $shape ) {
// Build our unique ID if we need to.
if ( empty( $new_settings[ $index ]['shapes'][ $shape_index ]['id'] ) && empty( $current_settings[ $index ]['shapes'][ $shape_index ]['id'] ) ) {
// Needs a unique ID.
$lowercase_name = str_replace( ' ', '-', strtolower( $shape['name'] ) );
$new_shapes[ $index ]['shapes'][ $shape_index ]['id'] = sanitize_key( $lowercase_name . '-' . uniqid() );
} elseif ( ! empty( $new_settings[ $index ]['shapes'][ $shape_index ]['id'] ) && empty( $current_settings[ $index ]['shapes'][ $shape_index ]['id'] ) ) {
// Has a unique ID but hasn't saved it yet (imported).
$new_shapes[ $index ]['shapes'][ $shape_index ]['id'] = sanitize_key( $new_settings[ $index ]['shapes'][ $shape_index ]['id'] );
} else {
// Has a unique ID, keep it.
$new_shapes[ $index ]['shapes'][ $shape_index ]['id'] = sanitize_key( $current_settings[ $index ]['shapes'][ $shape_index ]['id'] );
}
// Sanitize our name.
$new_shapes[ $index ]['shapes'][ $shape_index ]['name'] = sanitize_text_field( $shape['name'] );
// Sanitize our SVG.
if ( current_user_can( 'unfiltered_html' ) ) {
$new_shapes[ $index ]['shapes'][ $shape_index ]['shape'] = $shape['shape'];
} else {
$new_shapes[ $index ]['shapes'][ $shape_index ]['shape'] = wp_kses( $shape['shape'], generateblocks_pro_kses_svg() );
}
}
} else {
unset( $new_shapes[ $index ] );
}
}
if ( is_array( $new_shapes ) ) {
update_option( 'generateblocks_svg_shapes', $new_shapes );
}
return $this->success( __( 'Shapes saved.', 'generateblocks-pro' ) );
}
/**
* Update Icons.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function update_icon_settings( WP_REST_Request $request ) {
$current_settings = get_option( 'generateblocks_svg_icons', array() );
$new_settings = $request->get_param( 'settings' );
$new_icons = array();
$usedGroupNames = array();
foreach ( $new_settings as $index => $data ) {
// Build our group name and ID.
if ( ! in_array( $data['group'], $usedGroupNames ) ) {
$new_icons[ $index ]['group'] = sanitize_text_field( $data['group'] );
$new_icons[ $index ]['group_id'] = str_replace( ' ', '-', strtolower( $new_icons[ $index ]['group'] ) );
$usedGroupNames[] = $new_icons[ $index ]['group'];
} else {
$new_icons[ $index ]['group'] = sanitize_text_field( $data['group'] . ' ' . $index );
$new_icons[ $index ]['group_id'] = str_replace( ' ', '-', strtolower( $new_icons[ $index ]['group'] ) );
$usedGroupNames[] = $new_icons[ $index ]['group'];
}
if ( ! empty( $data['icons'] ) ) {
foreach ( $data['icons'] as $icon_index => $icon ) {
// Build our unique ID if we need to.
if ( empty( $new_settings[ $index ]['icons'][ $icon_index ]['id'] ) && empty( $current_settings[ $index ]['icons'][ $icon_index ]['id'] ) ) {
// Needs a unique ID.
$lowercase_name = str_replace( ' ', '-', strtolower( $icon['name'] ) );
$new_icons[ $index ]['icons'][ $icon_index ]['id'] = sanitize_key( $lowercase_name . '-' . uniqid() );
} elseif ( ! empty( $new_settings[ $index ]['icons'][ $icon_index ]['id'] ) && empty( $current_settings[ $index ]['icons'][ $icon_index ]['id'] ) ) {
// Has a unique ID but hasn't saved it yet (imported).
$new_icons[ $index ]['icons'][ $icon_index ]['id'] = sanitize_key( $new_settings[ $index ]['icons'][ $icon_index ]['id'] );
} else {
// Has a unique ID, keep it.
$new_icons[ $index ]['icons'][ $icon_index ]['id'] = sanitize_key( $current_settings[ $index ]['icons'][ $icon_index ]['id'] );
}
// Sanitize our name.
$new_icons[ $index ]['icons'][ $icon_index ]['name'] = sanitize_text_field( $icon['name'] );
// Sanitize our SVG.
if ( current_user_can( 'unfiltered_html' ) ) {
$new_icons[ $index ]['icons'][ $icon_index ]['icon'] = $icon['icon'];
} else {
$new_icons[ $index ]['icons'][ $icon_index ]['icon'] = wp_kses( $icon['icon'], generateblocks_pro_kses_svg() );
}
}
} else {
unset( $new_icons[ $index ] );
}
}
if ( is_array( $new_icons ) ) {
update_option( 'generateblocks_svg_icons', $new_icons );
}
return $this->success( __( 'Icons saved.', 'generateblocks-pro' ) );
}
/**
* Export a group of assets.
*
* @param WP_REST_Request $request request object.
*
* @return mixed
*/
public function export_asset_group( WP_REST_Request $request ) {
$asset_type = $request->get_param( 'assetType' );
$group_name = $request->get_param( 'groupName' );
$assets = $request->get_param( 'assets' );
$data = array(
'type' => $asset_type,
'group' => $group_name,
'assets' => $assets,
);
return $this->success( $data );
}
/**
* Success rest.
*
* @param mixed $response response data.
* @return mixed
*/
public function success( $response ) {
return new WP_REST_Response(
array(
'success' => true,
'response' => $response,
),
200
);
}
/**
* Failed rest.
*
* @param mixed $response response data.
* @return mixed
*/
public function failed( $response ) {
return new WP_REST_Response(
array(
'success' => false,
'response' => $response,
),
200
);
}
/**
* Error rest.
*
* @param mixed $code error code.
* @param mixed $response response data.
* @return mixed
*/
public function error( $code, $response ) {
return new WP_REST_Response(
array(
'error' => true,
'success' => false,
'error_code' => $code,
'response' => $response,
),
401
);
}
}
GenerateBlocks_Pro_Rest::get_instance();
@@ -0,0 +1,121 @@
<?php
/**
* General actions and filters.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Build our settings area.
*
* @since 1.0
*/
class GenerateBlocks_Pro_Settings {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initiate our class.
*/
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'generateblocks_settings_area', array( $this, 'add_license_key_area' ), 5 );
add_action( 'generateblocks_settings_area', array( $this, 'add_template_library_settings' ), 20 );
}
/**
* Enqueue our scripts.
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ( 'generateblocks_page_generateblocks-settings' === $screen->id ) {
$assets = generateblocks_pro_get_enqueue_assets(
'dashboard',
[
'dependencies' => array( 'generateblocks-settings', 'wp-api', 'wp-i18n', 'wp-components', 'wp-element', 'wp-api-fetch' ),
'version' => GENERATEBLOCKS_PRO_VERSION,
]
);
wp_enqueue_script(
'generateblocks-pro-settings',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/dashboard.js',
$assets['dependencies'],
$assets['version'],
true
);
wp_set_script_translations( 'generateblocks-pro-settings', 'generateblocks-pro', GENERATEBLOCKS_PRO_DIR . 'languages' );
wp_localize_script(
'generateblocks-pro-settings',
'generateBlocksProSettings',
array(
'settings' => wp_parse_args(
get_option( 'generateblocks', array() ),
generateblocks_get_option_defaults()
),
'adminSettings' => wp_parse_args(
get_option( 'generateblocks_admin', array() ),
generateblocks_pro_get_admin_option_defaults()
),
'licenseSettings' => wp_parse_args(
get_option( 'generateblocks_pro_licensing', array() ),
generateblocks_pro_get_license_defaults()
),
'useLegacyPatternLibrary' => generateblocks_pro_has_legacy_patterns(),
)
);
wp_enqueue_style(
'generateblocks-pro-settings',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/dashboard.css',
array( 'wp-components' ),
GENERATEBLOCKS_PRO_VERSION
);
}
}
/**
* Add license key container.
*
* @since 1.2.0
*/
public function add_license_key_area() {
echo '<div id="gblocks-license-key-settings"></div>';
}
/**
* Add Template Library container.
*
* @since 1.2.0
*/
public function add_template_library_settings() {
echo '<div id="gblocks-template-library-settings"></div>';
}
}
GenerateBlocks_Pro_Settings::get_instance();
@@ -0,0 +1,58 @@
<?php
/**
* GenerateBlocks Pro singleton class.
*
* @package Generateblocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The Singleton class.
*/
abstract class GenerateBlocks_Pro_Singleton {
/**
* Instance.
*
* @access private
* @var array Instances
*/
private static $instances = array();
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
protected function __construct() { }
/**
* Singletons should not be cloneable.
*/
protected function __clone() { }
/**
* Singletons should not be restorable from strings.
*
* @throws Exception Cannot unserialize a singleton.
*/
public function __wakeup() {
throw new Exception( 'Cannot unserialize a singleton.' );
}
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
$cls = static::class;
if ( ! isset( self::$instances[ $cls ] ) ) {
self::$instances[ $cls ] = new static();
}
return self::$instances[ $cls ];
}
}
@@ -0,0 +1,676 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Allows plugins to use their own update API.
*
* @author Easy Digital Downloads
* @version 1.9.2
*/
class GenerateBlocks_Plugin_Updater {
private $api_url = '';
private $api_data = array();
private $plugin_file = '';
private $name = '';
private $slug = '';
private $version = '';
private $wp_override = false;
private $beta = false;
private $failed_request_cache_key;
/**
* Class constructor.
*
* @uses plugin_basename()
* @uses hook()
*
* @param string $_api_url The URL pointing to the custom API endpoint.
* @param string $_plugin_file Path to the plugin file.
* @param array $_api_data Optional data to send with API calls.
*/
public function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
global $edd_plugin_data;
$this->api_url = trailingslashit( $_api_url );
$this->api_data = $_api_data;
$this->plugin_file = $_plugin_file;
$this->name = plugin_basename( $_plugin_file );
$this->slug = basename( $_plugin_file, '.php' );
$this->version = $_api_data['version'];
$this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
$this->beta = ! empty( $this->api_data['beta'] ) ? true : false;
$this->failed_request_cache_key = 'edd_sl_failed_http_' . md5( $this->api_url );
$edd_plugin_data[ $this->slug ] = $this->api_data;
/**
* Fires after the $edd_plugin_data is setup.
*
* @since x.x.x
*
* @param array $edd_plugin_data Array of EDD SL plugin data.
*/
do_action( 'post_edd_sl_plugin_updater_setup', $edd_plugin_data );
// Set up hooks.
$this->init();
}
/**
* Set up WordPress filters to hook into WP's update process.
*
* @uses add_filter()
*
* @return void
*/
public function init() {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
add_action( 'after_plugin_row', array( $this, 'show_update_notification' ), 10, 2 );
add_action( 'admin_init', array( $this, 'show_changelog' ) );
}
/**
* Check for Updates at the defined API endpoint and modify the update array.
*
* This function dives into the update API just when WordPress creates its update array,
* then adds a custom API call and injects the custom plugin data retrieved from the API.
* It is reassembled from parts of the native WordPress plugin update code.
* See wp-includes/update.php line 121 for the original wp_update_plugins() function.
*
* @uses api_request()
*
* @param array $_transient_data Update array build by WordPress.
* @return array Modified update array with custom plugin data.
*/
public function check_update( $_transient_data ) {
global $pagenow;
if ( ! is_object( $_transient_data ) ) {
$_transient_data = new stdClass();
}
if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
return $_transient_data;
}
$current = $this->get_repo_api_data();
if ( false !== $current && is_object( $current ) && isset( $current->new_version ) ) {
if ( version_compare( $this->version, $current->new_version, '<' ) ) {
$_transient_data->response[ $this->name ] = $current;
} else {
// Populating the no_update information is required to support auto-updates in WordPress 5.5.
$_transient_data->no_update[ $this->name ] = $current;
}
}
$_transient_data->last_checked = time();
$_transient_data->checked[ $this->name ] = $this->version;
return $_transient_data;
}
/**
* Get repo API data from store.
* Save to cache.
*
* @return \stdClass
*/
public function get_repo_api_data() {
$version_info = $this->get_cached_version_info();
if ( false === $version_info ) {
$version_info = $this->api_request(
'plugin_latest_version',
array(
'slug' => $this->slug,
'beta' => $this->beta,
)
);
if ( ! $version_info ) {
return false;
}
// This is required for your plugin to support auto-updates in WordPress 5.5.
$version_info->plugin = $this->name;
$version_info->id = $this->name;
$version_info->tested = $this->get_tested_version( $version_info );
$this->set_version_info_cache( $version_info );
}
return $version_info;
}
/**
* Gets the plugin's tested version.
*
* @since 1.9.2
* @param object $version_info
* @return null|string
*/
private function get_tested_version( $version_info ) {
// There is no tested version.
if ( empty( $version_info->tested ) ) {
return null;
}
// Strip off extra version data so the result is x.y or x.y.z.
list( $current_wp_version ) = explode( '-', get_bloginfo( 'version' ) );
// The tested version is greater than or equal to the current WP version, no need to do anything.
if ( version_compare( $version_info->tested, $current_wp_version, '>=' ) ) {
return $version_info->tested;
}
$current_version_parts = explode( '.', $current_wp_version );
$tested_parts = explode( '.', $version_info->tested );
// The current WordPress version is x.y.z, so update the tested version to match it.
if ( isset( $current_version_parts[2] ) && $current_version_parts[0] === $tested_parts[0] && $current_version_parts[1] === $tested_parts[1] ) {
$tested_parts[2] = $current_version_parts[2];
}
return implode( '.', $tested_parts );
}
/**
* Show the update notification on multisite subsites.
*
* @param string $file
* @param array $plugin
*/
public function show_update_notification( $file, $plugin ) {
// Return early if in the network admin, or if this is not a multisite install.
if ( is_network_admin() || ! is_multisite() ) {
return;
}
// Allow single site admins to see that an update is available.
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
if ( $this->name !== $file ) {
return;
}
// Do not print any message if update does not exist.
$update_cache = get_site_transient( 'update_plugins' );
if ( ! isset( $update_cache->response[ $this->name ] ) ) {
if ( ! is_object( $update_cache ) ) {
$update_cache = new stdClass();
}
$update_cache->response[ $this->name ] = $this->get_repo_api_data();
}
// Return early if this plugin isn't in the transient->response or if the site is running the current or newer version of the plugin.
if ( empty( $update_cache->response[ $this->name ] ) || version_compare( $this->version, $update_cache->response[ $this->name ]->new_version, '>=' ) ) {
return;
}
printf(
'<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">',
$this->slug,
$file,
in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive'
);
echo '<td colspan="3" class="plugin-update colspanchange">';
echo '<div class="update-message notice inline notice-warning notice-alt"><p>';
$changelog_link = '';
if ( ! empty( $update_cache->response[ $this->name ]->sections->changelog ) ) {
$changelog_link = add_query_arg(
array(
'edd_sl_action' => 'view_plugin_changelog',
'plugin' => urlencode( $this->name ),
'slug' => urlencode( $this->slug ),
'TB_iframe' => 'true',
'width' => 77,
'height' => 911,
),
self_admin_url( 'index.php' )
);
}
$update_link = add_query_arg(
array(
'action' => 'upgrade-plugin',
'plugin' => urlencode( $this->name ),
),
self_admin_url( 'update.php' )
);
printf(
/* translators: the plugin name. */
esc_html__( 'There is a new version of %1$s available.', 'easy-digital-downloads' ),
esc_html( $plugin['Name'] )
);
if ( ! current_user_can( 'update_plugins' ) ) {
echo ' ';
esc_html_e( 'Contact your network administrator to install the update.', 'easy-digital-downloads' );
} elseif ( empty( $update_cache->response[ $this->name ]->package ) && ! empty( $changelog_link ) ) {
echo ' ';
printf(
/* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate. */
__( '%1$sView version %2$s details%3$s.', 'easy-digital-downloads' ),
'<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
esc_html( $update_cache->response[ $this->name ]->new_version ),
'</a>'
);
} elseif ( ! empty( $changelog_link ) ) {
echo ' ';
printf(
__( '%1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'easy-digital-downloads' ),
'<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
esc_html( $update_cache->response[ $this->name ]->new_version ),
'</a>',
'<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
'</a>'
);
} else {
printf(
' %1$s%2$s%3$s',
'<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
esc_html__( 'Update now.', 'easy-digital-downloads' ),
'</a>'
);
}
do_action( "in_plugin_update_message-{$file}", $plugin, $plugin );
echo '</p></div></td></tr>';
}
/**
* Gets the plugins active in a multisite network.
*
* @return array
*/
private function get_active_plugins() {
$active_plugins = (array) get_option( 'active_plugins' );
$active_network_plugins = (array) get_site_option( 'active_sitewide_plugins' );
return array_merge( $active_plugins, array_keys( $active_network_plugins ) );
}
/**
* Updates information on the "View version x.x details" page with custom data.
*
* @uses api_request()
*
* @param mixed $_data
* @param string $_action
* @param object $_args
* @return object $_data
*/
public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
if ( 'plugin_information' !== $_action ) {
return $_data;
}
if ( ! isset( $_args->slug ) || ( $_args->slug !== $this->slug ) ) {
return $_data;
}
$to_send = array(
'slug' => $this->slug,
'is_ssl' => is_ssl(),
'fields' => array(
'banners' => array(),
'reviews' => false,
'icons' => array(),
),
);
// Get the transient where we store the api request for this plugin for 24 hours
$edd_api_request_transient = $this->get_cached_version_info();
//If we have no transient-saved value, run the API, set a fresh transient with the API value, and return that value too right now.
if ( empty( $edd_api_request_transient ) ) {
$api_response = $this->api_request( 'plugin_information', $to_send );
// Expires in 3 hours
$this->set_version_info_cache( $api_response );
if ( false !== $api_response ) {
$_data = $api_response;
}
} else {
$_data = $edd_api_request_transient;
}
// Convert sections into an associative array, since we're getting an object, but Core expects an array.
if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
$_data->sections = $this->convert_object_to_array( $_data->sections );
}
// Convert banners into an associative array, since we're getting an object, but Core expects an array.
if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
$_data->banners = $this->convert_object_to_array( $_data->banners );
}
// Convert icons into an associative array, since we're getting an object, but Core expects an array.
if ( isset( $_data->icons ) && ! is_array( $_data->icons ) ) {
$_data->icons = $this->convert_object_to_array( $_data->icons );
}
// Convert contributors into an associative array, since we're getting an object, but Core expects an array.
if ( isset( $_data->contributors ) && ! is_array( $_data->contributors ) ) {
$_data->contributors = $this->convert_object_to_array( $_data->contributors );
}
if ( ! isset( $_data->plugin ) ) {
$_data->plugin = $this->name;
}
return $_data;
}
/**
* Convert some objects to arrays when injecting data into the update API
*
* Some data like sections, banners, and icons are expected to be an associative array, however due to the JSON
* decoding, they are objects. This method allows us to pass in the object and return an associative array.
*
* @since 3.6.5
*
* @param stdClass $data
*
* @return array
*/
private function convert_object_to_array( $data ) {
if ( ! is_array( $data ) && ! is_object( $data ) ) {
return array();
}
$new_data = array();
foreach ( $data as $key => $value ) {
$new_data[ $key ] = is_object( $value ) ? $this->convert_object_to_array( $value ) : $value;
}
return $new_data;
}
/**
* Disable SSL verification in order to prevent download update failures
*
* @param array $args
* @param string $url
* @return object $array
*/
public function http_request_args( $args, $url ) {
if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
$args['sslverify'] = $this->verify_ssl();
}
return $args;
}
/**
* Calls the API and, if successfull, returns the object delivered by the API.
*
* @uses get_bloginfo()
* @uses wp_remote_post()
* @uses is_wp_error()
*
* @param string $_action The requested action.
* @param array $_data Parameters for the API action.
* @return false|object|void
*/
private function api_request( $_action, $_data ) {
$data = array_merge( $this->api_data, $_data );
if ( $data['slug'] !== $this->slug ) {
return;
}
// Don't allow a plugin to ping itself
if ( trailingslashit( home_url() ) === $this->api_url ) {
return false;
}
if ( $this->request_recently_failed() ) {
return false;
}
return $this->get_version_from_remote();
}
/**
* Determines if a request has recently failed.
*
* @since 1.9.1
*
* @return bool
*/
private function request_recently_failed() {
$failed_request_details = get_option( $this->failed_request_cache_key );
// Request has never failed.
if ( empty( $failed_request_details ) || ! is_numeric( $failed_request_details ) ) {
return false;
}
/*
* Request previously failed, but the timeout has expired.
* This means we're allowed to try again.
*/
if ( time() > $failed_request_details ) {
delete_option( $this->failed_request_cache_key );
return false;
}
return true;
}
/**
* Logs a failed HTTP request for this API URL.
* We set a timestamp for 1 hour from now. This prevents future API requests from being
* made to this domain for 1 hour. Once the timestamp is in the past, API requests
* will be allowed again. This way if the site is down for some reason we don't bombard
* it with failed API requests.
*
* @see EDD_SL_Plugin_Updater::request_recently_failed
*
* @since 1.9.1
*/
private function log_failed_request() {
update_option( $this->failed_request_cache_key, strtotime( '+1 hour' ) );
}
/**
* If available, show the changelog for sites in a multisite install.
*/
public function show_changelog() {
if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
return;
}
if ( empty( $_REQUEST['plugin'] ) ) {
return;
}
if ( empty( $_REQUEST['slug'] ) || $this->slug !== $_REQUEST['slug'] ) {
return;
}
if ( ! current_user_can( 'update_plugins' ) ) {
wp_die( esc_html__( 'You do not have permission to install plugin updates', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
}
$version_info = $this->get_repo_api_data();
if ( isset( $version_info->sections ) ) {
$sections = $this->convert_object_to_array( $version_info->sections );
if ( ! empty( $sections['changelog'] ) ) {
echo '<div style="background:#fff;padding:10px;">' . wp_kses_post( $sections['changelog'] ) . '</div>';
}
}
exit;
}
/**
* Gets the current version information from the remote site.
*
* @return array|false
*/
private function get_version_from_remote() {
$api_params = array(
'edd_action' => 'get_version',
'license' => ! empty( $this->api_data['license'] ) ? $this->api_data['license'] : '',
'item_name' => isset( $this->api_data['item_name'] ) ? $this->api_data['item_name'] : false,
'item_id' => isset( $this->api_data['item_id'] ) ? $this->api_data['item_id'] : false,
'version' => isset( $this->api_data['version'] ) ? $this->api_data['version'] : false,
'slug' => $this->slug,
'author' => $this->api_data['author'],
'url' => home_url(),
'beta' => $this->beta,
'php_version' => phpversion(),
'wp_version' => get_bloginfo( 'version' ),
);
/**
* Filters the parameters sent in the API request.
*
* @param array $api_params The array of data sent in the request.
* @param array $this->api_data The array of data set up in the class constructor.
* @param string $this->plugin_file The full path and filename of the file.
*/
$api_params = apply_filters( 'edd_sl_plugin_updater_api_params', $api_params, $this->api_data, $this->plugin_file );
$request = wp_remote_post(
$this->api_url,
array(
'timeout' => 15,
'sslverify' => $this->verify_ssl(),
'body' => $api_params,
)
);
if ( is_wp_error( $request ) || ( 200 !== wp_remote_retrieve_response_code( $request ) ) ) {
$this->log_failed_request();
return false;
}
$request = json_decode( wp_remote_retrieve_body( $request ) );
if ( $request && isset( $request->sections ) ) {
$request->sections = maybe_unserialize( $request->sections );
} else {
$request = false;
}
if ( $request && isset( $request->banners ) ) {
$request->banners = maybe_unserialize( $request->banners );
}
if ( $request && isset( $request->icons ) ) {
$request->icons = maybe_unserialize( $request->icons );
}
if ( ! empty( $request->sections ) ) {
foreach ( $request->sections as $key => $section ) {
$request->$key = (array) $section;
}
}
return $request;
}
/**
* Get the version info from the cache, if it exists.
*
* @param string $cache_key
* @return object
*/
public function get_cached_version_info( $cache_key = '' ) {
if ( empty( $cache_key ) ) {
$cache_key = $this->get_cache_key();
}
$cache = get_option( $cache_key );
// Cache is expired
if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
return false;
}
// We need to turn the icons into an array, thanks to WP Core forcing these into an object at some point.
$cache['value'] = json_decode( $cache['value'] );
if ( ! empty( $cache['value']->icons ) ) {
$cache['value']->icons = (array) $cache['value']->icons;
}
return $cache['value'];
}
/**
* Adds the plugin version information to the database.
*
* @param string $value
* @param string $cache_key
*/
public function set_version_info_cache( $value = '', $cache_key = '' ) {
if ( empty( $cache_key ) ) {
$cache_key = $this->get_cache_key();
}
$data = array(
'timeout' => strtotime( '+3 hours', time() ),
'value' => wp_json_encode( $value ),
);
update_option( $cache_key, $data, 'no' );
// Delete the duplicate option
delete_option( 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ) );
}
/**
* Returns if the SSL of the store should be verified.
*
* @since 1.6.13
* @return bool
*/
private function verify_ssl() {
return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
}
/**
* Gets the unique key (option name) for a plugin.
*
* @since 1.9.0
* @return string
*/
private function get_cache_key() {
$string = $this->slug . $this->api_data['license'] . $this->beta;
return 'edd_sl_' . md5( serialize( $string ) );
}
}
@@ -0,0 +1,907 @@
<?php
/**
* Abstract Condition Base Class.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract base class for conditions.
*/
abstract class GenerateBlocks_Pro_Condition_Abstract implements GenerateBlocks_Pro_Condition_Interface {
/**
* Get default rule metadata.
*
* @return array
*/
protected function get_default_rule_metadata() {
return [
'needs_value' => true,
'value_type' => 'text',
];
}
/**
* Parse custom field value structure with edge case handling.
*
* @param string $value The value to parse.
* @return array
*/
protected function parse_custom_value( $value ) {
// Handle null, empty, or non-string values.
if ( null === $value || '' === $value ) {
return [
'field_name' => '',
'comparison_value' => '',
];
}
// Convert to string if not already (handles numbers, booleans).
if ( ! is_string( $value ) ) {
if ( is_scalar( $value ) ) {
$value = (string) $value;
} else {
// Arrays, objects, resources - return empty.
return [
'field_name' => '',
'comparison_value' => '',
];
}
}
// Handle extremely long values to prevent memory issues.
if ( 10000 < strlen( $value ) ) {
$value = substr( $value, 0, 10000 );
}
if ( false === strpos( $value, '|' ) ) {
return [
'field_name' => sanitize_text_field( $value ),
'comparison_value' => '',
];
}
$parts = explode( '|', $value, 2 );
return [
'field_name' => sanitize_text_field( $parts[0] ),
'comparison_value' => sanitize_text_field( $parts[1] ?? '' ),
];
}
/**
* Sanitize custom field value with comprehensive validation.
*
* @param mixed $value The value to sanitize.
* @return string
*/
protected function sanitize_custom_value( $value ) {
if ( null === $value || '' === $value ) {
return '';
}
// Handle arrays or objects - reject them for custom fields.
if ( ! is_scalar( $value ) ) {
return '';
}
$value = (string) $value;
// Prevent extremely long values.
if ( 10000 < strlen( $value ) ) {
$value = substr( $value, 0, 10000 );
}
if ( false !== strpos( $value, '|' ) ) {
$parts = $this->parse_custom_value( $value );
// Validate field name (basic WordPress meta key validation).
$field_name = $parts['field_name'];
if ( empty( $field_name ) || 255 < strlen( $field_name ) ) {
return '';
}
// Only rebuild if we have a valid field name.
return $field_name . '|' . $parts['comparison_value'];
}
return sanitize_text_field( $value );
}
/**
* Standardized meta field parsing with edge case handling.
*
* @param string $rule The condition rule.
* @param mixed $value The condition value.
* @return array Parsed field data with 'field_name' and 'comparison_value'.
*/
protected function parse_meta_field( $rule, $value ) {
if ( 'custom' === $rule ) {
return $this->parse_custom_value( $value );
}
// Ensure rule is a valid string.
if ( ! is_string( $rule ) || empty( $rule ) ) {
return [
'field_name' => '',
'comparison_value' => '',
];
}
// Handle non-scalar values.
if ( ! is_scalar( $value ) && null !== $value ) {
$comparison_value = '';
} else {
$comparison_value = is_scalar( $value ) ? sanitize_text_field( $value ) : '';
}
return [
'field_name' => sanitize_text_field( $rule ),
'comparison_value' => $comparison_value,
];
}
/**
* Enhanced meta existence evaluation with comprehensive validation.
*
* @param string $meta_type The meta type ('post', 'user', 'option').
* @param int $object_id The object ID (post ID, user ID, etc.).
* @param string $meta_key The meta key to check.
* @param string $operator The operator ('exists' or 'not_exists').
* @return bool
*/
protected function evaluate_meta_existence( $meta_type, $object_id, $meta_key, $operator ) {
// Validate meta type.
if ( ! in_array( $meta_type, [ 'post', 'user', 'option' ], true ) ) {
return false;
}
// Validate meta key.
if ( empty( $meta_key ) || ! is_string( $meta_key ) || 255 < strlen( $meta_key ) ) {
return false;
}
// Validate object ID for non-option types.
if ( 'option' !== $meta_type ) {
if ( ! is_numeric( $object_id ) || 1 > $object_id || PHP_INT_MAX < $object_id ) {
return false;
}
$object_id = absint( $object_id );
}
// Validate operator.
if ( ! in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return false;
}
try {
if ( 'option' === $meta_type ) {
$exists = false !== get_option( $meta_key, false );
} else {
$exists = metadata_exists( $meta_type, $object_id, $meta_key );
}
} catch ( Exception $e ) {
// Database errors, invalid meta types, etc.
return false;
}
return 'not_exists' === $operator ? ! $exists : $exists;
}
/**
* Enhanced meta value retrieval with comprehensive validation.
*
* @param string $meta_type The meta type ('post', 'user', 'option').
* @param int $object_id The object ID (post ID, user ID, etc.).
* @param string $meta_key The meta key.
* @param string $operator The condition operator.
* @param mixed $comparison_value The value to compare against.
* @return bool
*/
protected function evaluate_meta_value( $meta_type, $object_id, $meta_key, $operator, $comparison_value ) {
// Validate inputs using same validation as existence check.
if ( ! in_array( $meta_type, [ 'post', 'user', 'option' ], true ) ) {
return false;
}
if ( empty( $meta_key ) || ! is_string( $meta_key ) || 255 < strlen( $meta_key ) ) {
return false;
}
if ( 'option' !== $meta_type ) {
if ( ! is_numeric( $object_id ) || 1 > $object_id || PHP_INT_MAX < $object_id ) {
return false;
}
$object_id = absint( $object_id );
}
// Handle multi-value operators first.
if ( $this->is_multi_value_operator( $operator ) ) {
try {
if ( 'option' === $meta_type ) {
$meta_value = get_option( $meta_key );
} elseif ( 'post' === $meta_type ) {
$meta_value = get_post_meta( $object_id, $meta_key, true );
} elseif ( 'user' === $meta_type ) {
$meta_value = get_user_meta( $object_id, $meta_key, true );
} else {
return false;
}
} catch ( Exception $e ) {
return false;
}
return $this->evaluate_multi_value_meta( $operator, $meta_value, $comparison_value );
}
// Get single meta value with error handling.
try {
if ( 'option' === $meta_type ) {
$meta_value = get_option( $meta_key );
} elseif ( 'post' === $meta_type ) {
$meta_value = get_post_meta( $object_id, $meta_key, true );
} elseif ( 'user' === $meta_type ) {
$meta_value = get_user_meta( $object_id, $meta_key, true );
} else {
return false;
}
} catch ( Exception $e ) {
return false;
}
// Evaluate based on operator.
switch ( $operator ) {
case 'equals':
return $this->compare_values_equals( $meta_value, $comparison_value );
case 'contains':
return is_string( $meta_value ) && is_string( $comparison_value ) &&
false !== strpos( $meta_value, $comparison_value );
case 'not_contains':
return ! is_string( $meta_value ) || ! is_string( $comparison_value ) ||
false === strpos( $meta_value, $comparison_value );
case 'greater_than':
return $this->compare_numeric( $meta_value, $comparison_value, '>' );
case 'less_than':
return $this->compare_numeric( $meta_value, $comparison_value, '<' );
default:
return false;
}
}
/**
* Check if operator supports multiple values.
*
* @param string $operator The operator to check.
* @return bool
*/
protected function is_multi_value_operator( $operator ) {
$multi_operators = [ 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
return in_array( $operator, $multi_operators, true );
}
/**
* Parse value as array for multi-select operators with comprehensive validation.
*
* @param mixed $value The value to parse.
* @return array
*/
protected function parse_multi_value( $value ) {
if ( is_array( $value ) ) {
// Limit array size to prevent memory issues.
if ( 1000 < count( $value ) ) {
$value = array_slice( $value, 0, 1000 );
}
return array_filter( array_map( 'sanitize_text_field', $value ) );
}
if ( null === $value || '' === $value ) {
return [];
}
// Handle non-scalar values.
if ( ! is_scalar( $value ) ) {
return [];
}
$value = (string) $value;
// Prevent extremely long JSON strings.
if ( 50000 < strlen( $value ) ) {
return [];
}
// Try to decode as JSON first.
if ( '{' === $value[0] || '[' === $value[0] ) {
$decoded = json_decode( $value, true );
if ( is_array( $decoded ) ) {
// Limit decoded array size.
if ( 1000 < count( $decoded ) ) {
$decoded = array_slice( $decoded, 0, 1000 );
}
return array_filter( array_map( 'sanitize_text_field', $decoded ) );
}
}
// Fallback to single value.
return [ sanitize_text_field( $value ) ];
}
/**
* Generic multi-value condition evaluator with validation.
*
* @param string $operator The operator (includes_any, includes_all, etc.).
* @param mixed $condition_values The condition values (array or string).
* @param callable $match_callback Callback function to check if a value matches.
* @return bool
*/
protected function evaluate_multi_value_generic( $operator, $condition_values, $match_callback ) {
if ( ! is_callable( $match_callback ) ) {
return false;
}
if ( ! in_array( $operator, [ 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ], true ) ) {
return false;
}
$values = $this->parse_multi_value( $condition_values );
if ( empty( $values ) ) {
return false;
}
$matches = [];
foreach ( $values as $value ) {
try {
$matches[] = call_user_func( $match_callback, $value );
} catch ( Exception $e ) {
$matches[] = false;
}
}
// Apply operator logic.
switch ( $operator ) {
case 'includes_any':
return in_array( true, $matches, true );
case 'includes_all':
return ! in_array( false, $matches, true );
case 'excludes_any':
return ! in_array( true, $matches, true );
case 'excludes_all':
return in_array( false, $matches, true );
default:
return false;
}
}
/**
* Evaluate multi-value condition.
*
* @param string $operator The operator (includes_any, includes_all, etc.).
* @param mixed $target_value The current value to check against.
* @param mixed $condition_values The condition values (array or string).
* @return bool
*/
protected function evaluate_multi_value( $operator, $target_value, $condition_values ) {
return $this->evaluate_multi_value_generic(
$operator,
$condition_values,
function( $value ) use ( $target_value ) {
return $this->values_match( $target_value, $value );
}
);
}
/**
* Evaluate multi-value condition for arrays (like post terms).
*
* @param string $operator The operator (includes_any, includes_all, etc.).
* @param array $target_values Array of current values to check against.
* @param mixed $condition_values The condition values (array or string).
* @return bool
*/
protected function evaluate_multi_value_array( $operator, $target_values, $condition_values ) {
if ( ! is_array( $target_values ) ) {
return false;
}
return $this->evaluate_multi_value_generic(
$operator,
$condition_values,
function( $value ) use ( $target_values ) {
foreach ( $target_values as $target_value ) {
if ( $this->values_match( $target_value, $value ) ) {
return true;
}
}
return false;
}
);
}
/**
* Enhanced multi-value meta evaluation with error handling.
*
* @param string $operator The operator (includes_any, includes_all, etc.).
* @param mixed $meta_value The meta value (could be string, array, or object).
* @param mixed $condition_values The condition values (array or string).
* @return bool
*/
protected function evaluate_multi_value_meta( $operator, $meta_value, $condition_values ) {
// Convert meta value to array if it's not already.
$meta_values = [];
if ( is_array( $meta_value ) ) {
// Limit meta array size.
if ( 1000 < count( $meta_value ) ) {
$meta_value = array_slice( $meta_value, 0, 1000 );
}
$meta_values = array_map( 'sanitize_text_field', $meta_value );
} elseif ( null !== $meta_value && '' !== $meta_value ) {
if ( is_scalar( $meta_value ) ) {
$meta_value_string = (string) $meta_value;
// Try to decode JSON if it looks like JSON.
if ( ( '{' === $meta_value_string[0] || '[' === $meta_value_string[0] ) && 50000 > strlen( $meta_value_string ) ) {
$decoded = json_decode( $meta_value_string, true );
if ( is_array( $decoded ) ) {
if ( 1000 < count( $decoded ) ) {
$decoded = array_slice( $decoded, 0, 1000 );
}
$meta_values = array_map( 'sanitize_text_field', $decoded );
} else {
$meta_values = [ sanitize_text_field( $meta_value_string ) ];
}
} else {
$meta_values = [ sanitize_text_field( $meta_value_string ) ];
}
}
}
return $this->evaluate_multi_value_generic(
$operator,
$condition_values,
function( $condition_value ) use ( $meta_values ) {
foreach ( $meta_values as $current_meta_value ) {
if ( $this->values_match( $current_meta_value, $condition_value ) ) {
return true;
}
}
return false;
}
);
}
/**
* Check if two values match (with loose comparison).
*
* @param mixed $value1 First value.
* @param mixed $value2 Second value.
* @return bool
*/
protected function values_match( $value1, $value2 ) {
// Convert both to strings for comparison.
return (string) $value1 === (string) $value2;
}
/**
* Compare values with type juggling for equals operator.
*
* @param mixed $value1 First value.
* @param mixed $value2 Second value.
* @return bool
*/
protected function compare_values_equals( $value1, $value2 ) {
return $value1 == $value2; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
}
/**
* Compare numeric values with enhanced validation.
*
* @param mixed $value1 First value.
* @param mixed $value2 Second value.
* @param string $operator Comparison operator (>, <).
* @return bool
*/
protected function compare_numeric( $value1, $value2, $operator ) {
if ( ! in_array( $operator, [ '>', '<' ], true ) ) {
return false;
}
// Handle edge cases for numeric comparison.
if ( null === $value1 || null === $value2 ) {
return false;
}
if ( ! is_numeric( $value1 ) || ! is_numeric( $value2 ) ) {
return false;
}
// Check for potential overflow issues.
if ( ! is_finite( (float) $value1 ) || ! is_finite( (float) $value2 ) ) {
return false;
}
$float1 = floatval( $value1 );
$float2 = floatval( $value2 );
if ( '>' === $operator ) {
return $float1 > $float2;
} elseif ( '<' === $operator ) {
return $float1 < $float2;
}
return false;
}
/**
* Enhanced value sanitization with better validation.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'custom' === $rule ) {
return $this->sanitize_custom_value( $value );
}
// Handle array values for multi-select.
if ( is_array( $value ) ) {
if ( 1000 < count( $value ) ) {
$value = array_slice( $value, 0, 1000 );
}
return array_filter( array_map( 'sanitize_text_field', $value ) );
}
return sanitize_text_field( $value );
}
/**
* Enhanced query parameter validation.
*
* @param string $param Parameter name.
* @return mixed
*/
protected function get_query_param( $param ) {
if ( empty( $param ) || ! is_string( $param ) || 255 < strlen( $param ) ) {
return null;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET[ $param ] ) ) {
return null;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$value = wp_unslash( $_GET[ $param ] );
// Handle arrays in query params.
if ( is_array( $value ) ) {
if ( 100 < count( $value ) ) {
$value = array_slice( $value, 0, 100 );
}
return array_map( 'sanitize_text_field', $value );
}
// Prevent extremely long query values.
if ( is_string( $value ) && 10000 < strlen( $value ) ) {
$value = substr( $value, 0, 10000 );
}
return sanitize_text_field( $value );
}
/**
* Enhanced input validation for server variables.
*
* @param string $var Variable name.
* @return string
*/
protected function get_server_var( $var ) {
if ( empty( $var ) || ! is_string( $var ) || 100 < strlen( $var ) ) {
return '';
}
// Whitelist allowed server variables for security.
$allowed_vars = [
'HTTP_USER_AGENT',
'HTTP_REFERER',
'REQUEST_METHOD',
'REMOTE_ADDR',
'HTTP_HOST',
'REQUEST_URI',
'QUERY_STRING',
];
if ( ! in_array( $var, $allowed_vars, true ) ) {
return '';
}
if ( ! isset( $_SERVER[ $var ] ) ) {
return '';
}
$value = wp_unslash( $_SERVER[ $var ] );
// Type-specific validation for enhanced security.
switch ( $var ) {
case 'HTTP_REFERER':
// Validate and sanitize as URL.
$value = esc_url_raw( $value );
break;
case 'HTTP_USER_AGENT':
// Remove control characters and normalize.
$value = preg_replace( '/[\x00-\x1F\x7F]/', '', $value );
$value = sanitize_text_field( $value );
break;
default:
$value = sanitize_text_field( $value );
}
// Prevent extremely long values.
if ( is_string( $value ) && 10000 < strlen( $value ) ) {
$value = substr( $value, 0, 10000 );
}
return $value;
}
/**
* Enhanced cookie validation.
*
* @param string $cookie_name Cookie name.
* @return string|null
*/
protected function get_cookie_value( $cookie_name ) {
if ( empty( $cookie_name ) || ! is_string( $cookie_name ) || 255 < strlen( $cookie_name ) ) {
return null;
}
// Basic cookie name validation (prevent obvious XSS attempts).
// Also reject browser-reserved cookie prefixes (__Host-, __Secure-).
if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $cookie_name ) || 0 === strpos( $cookie_name, '__' ) ) {
return null;
}
if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
return null;
}
$value = wp_unslash( $_COOKIE[ $cookie_name ] );
// Prevent extremely long cookie values.
if ( is_string( $value ) && 4096 < strlen( $value ) ) {
$value = substr( $value, 0, 4096 );
}
return sanitize_text_field( $value );
}
/**
* Check if cookie exists with validation.
*
* @param string $cookie_name Cookie name.
* @return bool
*/
protected function cookie_exists( $cookie_name ) {
if ( empty( $cookie_name ) || ! is_string( $cookie_name ) || 255 < strlen( $cookie_name ) ) {
return false;
}
// Reject browser-reserved cookie prefixes.
if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $cookie_name ) || 0 === strpos( $cookie_name, '__' ) ) {
return false;
}
return isset( $_COOKIE[ $cookie_name ] );
}
/**
* Check if query parameter exists with validation.
*
* @param string $param_name Parameter name.
* @return bool
*/
protected function query_param_exists( $param_name ) {
if ( empty( $param_name ) || ! is_string( $param_name ) || 255 < strlen( $param_name ) ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET[ $param_name ] );
}
/**
* Enhanced meta value check - determines if meta has a non-empty value.
*
* @param string $meta_type The meta type ('post', 'user', 'option').
* @param int $object_id The object ID (post ID, user ID, etc.).
* @param string $meta_key The meta key.
* @return bool
*/
protected function evaluate_meta_has_value( $meta_type, $object_id, $meta_key ) {
// Validate inputs using same validation as existence check.
if ( ! in_array( $meta_type, [ 'post', 'user', 'option' ], true ) ) {
return false;
}
if ( empty( $meta_key ) || ! is_string( $meta_key ) || 255 < strlen( $meta_key ) ) {
return false;
}
if ( 'option' !== $meta_type ) {
if ( ! is_numeric( $object_id ) || 1 > $object_id || PHP_INT_MAX < $object_id ) {
return false;
}
$object_id = absint( $object_id );
}
// Get the value.
try {
if ( 'option' === $meta_type ) {
// For options, false means it doesn't exist.
if ( false === get_option( $meta_key, false ) ) {
return false;
}
$value = get_option( $meta_key );
} elseif ( 'post' === $meta_type ) {
// First check if it exists.
if ( ! metadata_exists( $meta_type, $object_id, $meta_key ) ) {
return false;
}
$value = get_post_meta( $object_id, $meta_key, true );
} elseif ( 'user' === $meta_type ) {
// First check if it exists.
if ( ! metadata_exists( $meta_type, $object_id, $meta_key ) ) {
return false;
}
$value = get_user_meta( $object_id, $meta_key, true );
} else {
return false;
}
} catch ( Exception $e ) {
return false;
}
// Check for various "empty" states.
if ( null === $value || '' === $value || false === $value ) {
return false;
}
// Empty array is also "no value".
if ( is_array( $value ) && 0 === count( $value ) ) {
return false;
}
// Everything else is considered "has value" (including 0 and "0").
return true;
}
/**
* Check if cookie has a non-empty value.
*
* @param string $cookie_name Cookie name.
* @return bool
*/
protected function cookie_has_value( $cookie_name ) {
if ( ! $this->cookie_exists( $cookie_name ) ) {
return false;
}
$value = $this->get_cookie_value( $cookie_name );
// Check for various "empty" states.
if ( null === $value || '' === $value || false === $value ) {
return false;
}
// Everything else is considered "has value" (including 0 and "0").
return true;
}
/**
* Check if query parameter has a non-empty value.
*
* @param string $param_name Parameter name.
* @return bool
*/
protected function query_param_has_value( $param_name ) {
if ( ! $this->query_param_exists( $param_name ) ) {
return false;
}
$value = $this->get_query_param( $param_name );
// Check for various "empty" states.
if ( null === $value || '' === $value || false === $value ) {
return false;
}
// Empty array is also "no value".
if ( is_array( $value ) && 0 === count( $value ) ) {
return false;
}
// Everything else is considered "has value" (including 0 and "0").
return true;
}
/**
* Check if referrer has a non-empty value.
*
* @return bool
*/
protected function referrer_has_value() {
$referrer = $this->get_server_var( 'HTTP_REFERER' );
// Check for various "empty" states.
if ( null === $referrer || '' === $referrer || false === $referrer ) {
return false;
}
// Everything else is considered "has value".
return true;
}
/**
* Check if operator needs a value.
*
* @param string $operator The operator.
* @return bool
*/
protected function operator_needs_value( $operator ) {
$no_value_operators = [ 'exists', 'not_exists', 'has_value', 'no_value' ];
return ! in_array( $operator, $no_value_operators, true );
}
/**
* Check if a rule actually supports multi-value selection.
*
* @param string $rule The rule key.
* @return bool
*/
protected function rule_supports_multi_select( $rule ) {
$metadata = $this->get_rule_metadata( $rule );
// Only support multi-select if we have object selectors.
$multi_select_types = [ 'object_selector', 'hierarchical_object_selector' ];
return in_array( $metadata['value_type'] ?? '', $multi_select_types, true );
}
/**
* Get operators with intelligent filtering based on actual multi-select support.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// Get base operators for this condition type.
$types = GenerateBlocks_Pro_Conditions::get_condition_types();
$class_name = get_class( $this );
$base_operators = [];
foreach ( $types as $type_data ) {
if ( $type_data['class'] === $class_name ) {
$base_operators = $type_data['operators'];
break;
}
}
// If this rule doesn't support multi-select, remove ALL "any/all" operators.
if ( ! $this->rule_supports_multi_select( $rule ) ) {
$multi_operators = [ 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
$base_operators = array_diff( $base_operators, $multi_operators );
}
return $base_operators;
}
}
@@ -0,0 +1,152 @@
<?php
/**
* Conditions Dashboard Admin Page
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class GenerateBlocks_Pro_Conditions_Dashboard
*/
class GenerateBlocks_Pro_Conditions_Dashboard {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'generateblocks_dashboard_tabs', [ $this, 'add_tab' ] );
add_filter( 'generateblocks_dashboard_screens', [ $this, 'add_to_dashboard_pages' ] );
}
/**
* Add admin menu page.
*/
public function add_admin_menu() {
// Get the required capability.
$capability = GenerateBlocks_Pro_Conditions::get_conditions_capability( 'manage' );
// Only add menu if user has permission.
if ( ! current_user_can( $capability ) ) {
return;
}
add_submenu_page(
'generateblocks',
__( 'Conditions', 'generateblocks-pro' ),
__( 'Conditions', 'generateblocks-pro' ),
$capability,
'generateblocks-conditions',
[ $this, 'render_dashboard' ],
4
);
}
/**
* Render the dashboard page.
*/
public function render_dashboard() {
// Double-check permission before rendering.
if ( ! GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'manage' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'generateblocks-pro' ) );
}
?>
<div class="wrap">
<div id="gb-conditions-dashboard"></div>
</div>
<?php
}
/**
* Enqueue scripts for the dashboard.
*
* @param string $hook_suffix The current admin page.
*/
public function enqueue_scripts( $hook_suffix ) {
if ( 'generateblocks_page_generateblocks-conditions' !== $hook_suffix ) {
return;
}
$assets = generateblocks_pro_get_enqueue_assets( 'conditions-dashboard' );
wp_enqueue_script(
'gb-conditions-dashboard',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/conditions-dashboard.js',
$assets['dependencies'],
$assets['version'],
true
);
wp_enqueue_style(
'gb-conditions-dashboard',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/conditions-dashboard.css',
[ 'wp-components', 'generateblocks-pro-dashboard-table' ],
GENERATEBLOCKS_PRO_VERSION
);
wp_localize_script(
'gb-conditions-dashboard',
'gbConditionsDashboard',
[
'apiUrl' => rest_url( 'wp/v2/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
]
);
}
/**
* Add a Local Templates tab to the GB Dashboard tabs.
*
* @param array $tabs The existing tabs.
*/
public function add_tab( $tabs ) {
$screen = get_current_screen();
$tabs['conditions'] = array(
'name' => __( 'Conditions', 'generateblocks-pro' ),
'url' => admin_url( 'admin.php?page=generateblocks-conditions' ),
'class' => 'generateblocks_page_generateblocks-conditions' === $screen->id ? 'active' : '',
);
return $tabs;
}
/**
* Add to our Dashboard pages.
*
* @since 1.0.0
* @param array $pages The existing pages.
*/
public function add_to_dashboard_pages( $pages ) {
$pages[] = 'generateblocks_page_generateblocks-conditions';
return $pages;
}
}
GenerateBlocks_Pro_Conditions_Dashboard::get_instance();
@@ -0,0 +1,305 @@
<?php
/**
* Conditions Post Type Registration
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class GenerateBlocks_Pro_Conditions_Post_Type
*/
class GenerateBlocks_Pro_Conditions_Post_Type {
/**
* Instance.
*
* @access private
* @var object Instance
*/
private static $instance;
/**
* Initiator.
*
* @return object initialized object of class.
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
public function __construct() {
add_action( 'init', [ $this, 'register_post_type' ] );
add_action( 'init', [ $this, 'register_taxonomy' ] );
add_action( 'init', [ $this, 'register_post_meta' ] );
}
/**
* Build the register_post_type() args.
*
* Capabilities are pinned to the 'manage' context of the conditions
* capability filter so WP core REST writes (/wp/v2/gblocks-conditions)
* require the same capability as the custom /advanced-conditions/v1/* routes.
* Without this, any edit_posts user (Author+) could create published
* condition posts via the core REST endpoint and bypass the 2.4.0
* manage_options hardening.
*
* @return array
*/
public function get_conditions_cpt_args() {
$labels = [
'name' => __( 'Conditions', 'generateblocks-pro' ),
'singular_name' => __( 'Condition', 'generateblocks-pro' ),
'menu_name' => __( 'Conditions', 'generateblocks-pro' ),
'add_new' => __( 'Add New', 'generateblocks-pro' ),
'add_new_item' => __( 'Add New Condition', 'generateblocks-pro' ),
'edit_item' => __( 'Edit Condition', 'generateblocks-pro' ),
'new_item' => __( 'New Condition', 'generateblocks-pro' ),
'view_item' => __( 'View Condition', 'generateblocks-pro' ),
'search_items' => __( 'Search Conditions', 'generateblocks-pro' ),
'not_found' => __( 'No conditions found.', 'generateblocks-pro' ),
'not_found_in_trash' => __( 'No conditions found in Trash.', 'generateblocks-pro' ),
];
$manage_cap = GenerateBlocks_Pro_Conditions::get_conditions_capability( 'manage' );
$use_cap = GenerateBlocks_Pro_Conditions::get_conditions_capability( 'use' );
return [
'labels' => $labels,
'public' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_in_admin_bar' => false,
'show_in_nav_menus' => false,
'can_export' => true,
'has_archive' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'capability_type' => 'post',
'map_meta_cap' => true,
// Override only primitive capabilities — leaving meta caps
// (edit_post/read_post/delete_post) at their defaults so WP's
// map_meta_cap() resolves them via these primitives. Do not map
// meta caps directly to a global cap like 'manage_options': WP's
// _post_type_meta_capabilities() would then register that global
// cap in $post_type_meta_caps and recursive-rewrite it to a meta
// cap with no post ID, which always resolves to do_not_allow.
'capabilities' => [
'edit_posts' => $manage_cap,
'edit_others_posts' => $manage_cap,
'delete_posts' => $manage_cap,
'delete_others_posts' => $manage_cap,
'delete_private_posts' => $manage_cap,
'delete_published_posts' => $manage_cap,
'edit_private_posts' => $manage_cap,
'edit_published_posts' => $manage_cap,
'publish_posts' => $manage_cap,
'read_private_posts' => $manage_cap,
'create_posts' => $manage_cap,
'read' => $use_cap,
],
'show_in_rest' => true,
'rest_base' => 'gblocks-conditions',
'supports' => [ 'title' ],
];
}
/**
* Register the conditions post type.
*/
public function register_post_type() {
register_post_type( 'gblocks_condition', $this->get_conditions_cpt_args() );
}
/**
* Build the register_taxonomy() args for the condition category taxonomy.
*
* The manage_terms/edit_terms/delete_terms caps are pinned to the manage context so
* WP core REST writes on /wp/v2/condition-categories (see WP_REST_Terms_Controller)
* require the same capability as the custom conditions routes. assign_terms
* stays at the use context so edit_posts users can still read the category
* list from the block editor UI.
*
* @return array
*/
public function get_conditions_taxonomy_args() {
$labels = [
'name' => __( 'Condition Categories', 'generateblocks-pro' ),
'singular_name' => __( 'Condition Category', 'generateblocks-pro' ),
'search_items' => __( 'Search Categories', 'generateblocks-pro' ),
'all_items' => __( 'All Categories', 'generateblocks-pro' ),
'parent_item' => __( 'Parent Category', 'generateblocks-pro' ),
'parent_item_colon' => __( 'Parent Category:', 'generateblocks-pro' ),
'edit_item' => __( 'Edit Category', 'generateblocks-pro' ),
'update_item' => __( 'Update Category', 'generateblocks-pro' ),
'add_new_item' => __( 'Add New Category', 'generateblocks-pro' ),
'new_item_name' => __( 'New Category Name', 'generateblocks-pro' ),
'menu_name' => __( 'Categories', 'generateblocks-pro' ),
];
$manage_cap = GenerateBlocks_Pro_Conditions::get_conditions_capability( 'manage' );
$use_cap = GenerateBlocks_Pro_Conditions::get_conditions_capability( 'use' );
return [
'labels' => $labels,
'hierarchical' => true,
'public' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_admin_column' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_in_rest' => true,
'rest_base' => 'condition-categories',
'capabilities' => [
'manage_terms' => $manage_cap,
'edit_terms' => $manage_cap,
'delete_terms' => $manage_cap,
'assign_terms' => $use_cap,
],
];
}
/**
* Register the condition category taxonomy.
*/
public function register_taxonomy() {
register_taxonomy( 'gblocks_condition_cat', [ 'gblocks_condition' ], $this->get_conditions_taxonomy_args() );
}
/**
* Build the register_post_meta() args for _gb_conditions.
*
* The sanitize callback must be callable by is_callable() — register_meta
* silently skips filter registration when that check fails. The previous
* [class-string, instance-method] form failed is_callable() and caused
* _gb_conditions meta to be stored without sanitization.
*
* auth_callback checks the manage capability so the REST meta endpoint
* matches the CPT-level capability requirements.
*
* @return array
*/
public function get_conditions_meta_args() {
return [
'single' => true,
'type' => 'object',
'auth_callback' => static function() {
return GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'manage' );
},
'sanitize_callback' => [ GenerateBlocks_Pro_Conditions::get_instance(), 'sanitize_conditions' ],
'show_in_rest' => [
'schema' => [
'type' => 'object',
'properties' => [
'logic' => [
'type' => 'string',
'enum' => [ 'AND', 'OR' ],
],
'groups' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'logic' => [
'type' => 'string',
'enum' => [ 'AND', 'OR' ],
],
'conditions' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'type' => [ 'type' => 'string' ],
'rule' => [ 'type' => 'string' ],
'operator' => [ 'type' => 'string' ],
'value' => [ 'type' => 'string' ],
],
],
],
],
],
],
],
],
],
];
}
/**
* Register post meta for conditions.
*/
public function register_post_meta() {
register_post_meta( 'gblocks_condition', '_gb_conditions', $this->get_conditions_meta_args() );
register_rest_field(
'gblocks_condition',
'gbConditions',
[
'get_callback' => function( $data ) {
$conditions = get_post_meta( $data['id'], '_gb_conditions', true );
return $conditions ? $conditions : [
'logic' => 'OR',
'groups' => [],
];
},
'update_callback' => function( $value, $post ) {
if ( ! GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'manage' ) ) {
return new \WP_Error(
'rest_cannot_update',
__( 'Sorry, you are not allowed to edit conditions.', 'generateblocks-pro' ),
[ 'status' => rest_authorization_required_code() ]
);
}
$sanitized = GenerateBlocks_Pro_Conditions::get_instance()->sanitize_conditions( $value );
update_post_meta( $post->ID, '_gb_conditions', $sanitized );
},
'schema' => [
'type' => 'object',
'properties' => [
'logic' => [
'type' => 'string',
'enum' => [ 'AND', 'OR' ],
],
'groups' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'logic' => [
'type' => 'string',
'enum' => [ 'AND', 'OR' ],
],
'conditions' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'type' => [ 'type' => 'string' ],
'rule' => [ 'type' => 'string' ],
'operator' => [ 'type' => 'string' ],
'value' => [ 'type' => 'string' ],
],
],
],
],
],
],
],
],
]
);
}
}
GenerateBlocks_Pro_Conditions_Post_Type::get_instance();
@@ -0,0 +1,338 @@
<?php
/**
* Conditions Registry
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registry for condition types.
*/
class GenerateBlocks_Pro_Conditions_Registry {
/**
* Registered condition types.
*
* @var array
*/
private static $condition_types = [];
/**
* Condition instances cache.
*
* @var array
*/
private static $instances = [];
/**
* Evaluation results cache (per-request).
*
* @var array
*/
private static $evaluation_cache = [];
/**
* Register a condition type.
*
* @param string $type Condition type identifier.
* @param array $args Condition arguments.
* @param string $classname Condition evaluator class name.
* @return bool
*/
public static function register( $type, $args, $classname ) {
if ( empty( $type ) || empty( $classname ) ) {
return false;
}
if ( ! class_exists( $classname ) ) {
return false;
}
// Ensure the class implements our interface.
$interfaces = class_implements( $classname );
if ( ! isset( $interfaces['GenerateBlocks_Pro_Condition_Interface'] ) ) {
return false;
}
$defaults = [
'label' => '',
'operators' => [],
'priority' => 10,
];
$args = wp_parse_args( $args, $defaults );
self::$condition_types[ $type ] = [
'label' => $args['label'],
'class' => $classname,
'operators' => $args['operators'],
'priority' => $args['priority'],
];
return true;
}
/**
* Unregister a condition type.
*
* @param string $type Condition type identifier.
* @return bool
*/
public static function unregister( $type ) {
if ( ! isset( self::$condition_types[ $type ] ) ) {
return false;
}
unset( self::$condition_types[ $type ] );
if ( isset( self::$instances[ $type ] ) ) {
unset( self::$instances[ $type ] );
}
return true;
}
/**
* Get all registered condition types.
*
* @return array
*/
public static function get_all() {
// Sort by priority.
uasort(
self::$condition_types,
function( $a, $b ) {
return $a['priority'] <=> $b['priority'];
}
);
return self::$condition_types;
}
/**
* Get a specific condition type.
*
* @param string $type Condition type identifier.
* @return array|null
*/
public static function get( $type ) {
return isset( self::$condition_types[ $type ] ) ? self::$condition_types[ $type ] : null;
}
/**
* Get condition instance.
*
* @param string $type Condition type identifier.
* @return GenerateBlocks_Pro_Condition_Interface|null
*/
public static function get_instance( $type ) {
if ( ! isset( self::$condition_types[ $type ] ) ) {
return null;
}
if ( ! isset( self::$instances[ $type ] ) ) {
$classname = self::$condition_types[ $type ]['class'];
self::$instances[ $type ] = new $classname();
}
return self::$instances[ $type ];
}
/**
* Evaluate a condition.
*
* @param string $type Condition type.
* @param string $rule Condition rule.
* @param string $operator Condition operator.
* @param mixed $value Condition value.
* @param array $context Additional context.
* @return bool
*/
public static function evaluate( $type, $rule, $operator, $value, $context = [] ) {
$instance = self::get_instance( $type );
if ( ! $instance ) {
return false;
}
// Cache key for performance - per-request only using class property.
$cache_data = wp_json_encode( [ $type, $rule, $operator, $value, $context ] );
$cache_key = md5( $cache_data );
if ( isset( self::$evaluation_cache[ $cache_key ] ) ) {
return self::$evaluation_cache[ $cache_key ];
}
$result = $instance->evaluate( $rule, $operator, $value, $context );
// Cache for current request only using class property.
self::$evaluation_cache[ $cache_key ] = $result;
return $result;
}
/**
* Clear the evaluation cache.
* Used primarily for testing purposes to reset state between tests.
*
* @return void
*/
public static function clear_evaluation_cache() {
self::$evaluation_cache = [];
}
/**
* Register core condition types.
*/
public static function register_core_types() {
// Location.
self::register(
'location',
[
'label' => __( 'Location', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ],
'priority' => 10,
],
'GenerateBlocks_Pro_Condition_Location'
);
// Query Parameter.
self::register(
'query_arg',
[
'label' => __( 'Query Parameter', 'generateblocks-pro' ),
'operators' => [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'starts_with', 'ends_with' ],
'priority' => 20,
],
'GenerateBlocks_Pro_Condition_Query_Arg'
);
// User Role.
self::register(
'user_role',
[
'label' => __( 'User Role', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not', 'includes_any', 'includes_all' ],
'priority' => 30,
],
'GenerateBlocks_Pro_Condition_User_Role'
);
// Date & Time.
self::register(
'date_time',
[
'label' => __( 'Date & Time', 'generateblocks-pro' ),
'operators' => [ 'before', 'after', 'between', 'on' ],
'priority' => 40,
],
'GenerateBlocks_Pro_Condition_Date_Time'
);
// Device.
self::register(
'device',
[
'label' => __( 'Device', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not' ],
'priority' => 50,
],
'GenerateBlocks_Pro_Condition_Device'
);
// Referrer.
self::register(
'referrer',
[
'label' => __( 'Referrer', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not', 'contains', 'not_contains', 'equals', 'starts_with', 'ends_with' ],
'priority' => 60,
],
'GenerateBlocks_Pro_Condition_Referrer'
);
// Post Meta.
self::register(
'post_meta',
[
'label' => __( 'Post Meta', 'generateblocks-pro' ),
'operators' => [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'includes_any', 'includes_all' ],
'priority' => 70,
],
'GenerateBlocks_Pro_Condition_Post_Meta'
);
// User Meta.
self::register(
'user_meta',
[
'label' => __( 'User Meta', 'generateblocks-pro' ),
'operators' => [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'includes_any', 'includes_all' ],
'priority' => 80,
],
'GenerateBlocks_Pro_Condition_User_Meta'
);
// Cookie.
self::register(
'cookie',
[
'label' => __( 'Cookie', 'generateblocks-pro' ),
'operators' => [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'starts_with', 'ends_with' ],
'priority' => 90,
],
'GenerateBlocks_Pro_Condition_Cookie'
);
// Language.
self::register(
'language',
[
'label' => __( 'Language', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not' ],
'priority' => 95,
],
'GenerateBlocks_Pro_Condition_Language'
);
// Options.
self::register(
'options',
[
'label' => __( 'Site Options', 'generateblocks-pro' ),
'operators' => [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'includes_any', 'includes_all' ],
'priority' => 100,
],
'GenerateBlocks_Pro_Condition_Options'
);
// Author.
self::register(
'author',
[
'label' => __( 'Author', 'generateblocks-pro' ),
'operators' => [ 'is', 'is_not', 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'includes_any', 'excludes_any' ],
'priority' => 110,
],
'GenerateBlocks_Pro_Condition_Author'
);
}
/**
* Reset registry for testing purposes only.
*
* @since 2.2.0
*/
public static function reset_for_testing() {
// Only allow in testing environment.
if ( ! defined( 'GB_TESTING' ) || ! GB_TESTING ) {
return;
}
self::$condition_types = [];
self::$instances = [];
}
}
@@ -0,0 +1,390 @@
<?php
/**
* The Advanced Conditions class file.
*
* @package GenerateBlocksTheme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Main class for advanced conditions system.
*
* @since 1.0.0
*/
class GenerateBlocks_Pro_Conditions extends GenerateBlocks_Pro_Singleton {
/**
* Initialize the conditions system.
*/
public function init() {
// Register core condition types early for REST API, but after init for translations.
add_action( 'init', [ $this, 'register_core_conditions' ], 0 );
// Allow third-party registrations after core.
add_action(
'init',
function() {
do_action( 'generateblocks_register_conditions' );
},
10
);
}
/**
* Get the capability required for conditions.
*
* @since 2.4.0
*
* Breaking changes in 2.4.0:
* - Previously all operations required only 'edit_posts' capability
* - Now 'manage' context (create/edit/delete) requires 'manage_options' by default
* - Use the 'generateblocks_conditions_capability' filter to customize
*
* @param string $context The context: 'use' (select existing) or 'manage' (create/edit/dashboard).
* @return string The capability required.
*/
public static function get_conditions_capability( $context = 'use' ) {
// Default capabilities.
if ( 'manage' === $context ) {
// Can create/edit/access dashboard - default to manage_options.
$capability = 'manage_options';
} else {
// Can select/use existing conditions - anyone who can edit posts.
$capability = 'edit_posts';
}
/**
* Filter the capability required for conditions.
*
* @since 2.4.0
* @param string $capability The capability required.
* @param string $context The context: 'use' or 'manage'.
*/
return apply_filters( 'generateblocks_conditions_capability', $capability, $context );
}
/**
* Check if current user can use conditions.
*
* @since 2.4.0
* @param string $context The context: 'use' (select existing) or 'manage' (create/edit/dashboard).
* @return bool
*/
public static function current_user_can_use_conditions( $context = 'use' ) {
$capability = self::get_conditions_capability( $context );
return current_user_can( $capability );
}
/**
* Register core condition types.
*/
public function register_core_conditions() {
// Check if already registered to avoid duplicates.
if ( ! empty( GenerateBlocks_Pro_Conditions_Registry::get_all() ) ) {
return;
}
GenerateBlocks_Pro_Conditions_Registry::register_core_types();
}
/**
* Sanitize conditions data.
*
* @param array $meta_value The conditions array.
* @return array
*/
public function sanitize_conditions( $meta_value ) {
if ( ! is_array( $meta_value ) ) {
return [
'logic' => 'OR',
'groups' => [],
];
}
$sanitized = [
'logic' => in_array( $meta_value['logic'] ?? 'OR', [ 'AND', 'OR' ], true ) ? $meta_value['logic'] : 'OR',
'groups' => [],
];
if ( isset( $meta_value['groups'] ) && is_array( $meta_value['groups'] ) ) {
foreach ( $meta_value['groups'] as $group ) {
if ( ! is_array( $group ) ) {
continue;
}
$sanitized_group = [
'logic' => in_array( $group['logic'] ?? 'AND', [ 'AND', 'OR' ], true ) ? $group['logic'] : 'AND',
'conditions' => [],
];
if ( isset( $group['conditions'] ) && is_array( $group['conditions'] ) ) {
foreach ( $group['conditions'] as $condition ) {
if ( ! is_array( $condition ) ) {
continue;
}
$sanitized_condition = [
'type' => sanitize_text_field( $condition['type'] ?? '' ),
'rule' => sanitize_text_field( $condition['rule'] ?? '' ),
'operator' => sanitize_text_field( $condition['operator'] ?? '' ),
'value' => $this->sanitize_condition_value( $condition ),
];
$sanitized_group['conditions'][] = $sanitized_condition;
}
}
$sanitized['groups'][] = $sanitized_group;
}
}
return $sanitized;
}
/**
* Sanitize condition value - handles custom field two-part values and arrays
*
* @param array $condition The condition array.
* @return string|array
*/
private function sanitize_condition_value( $condition ) {
$value = $condition['value'] ?? '';
if ( empty( $value ) ) {
return '';
}
// Handle array values for multi-select operators.
if ( is_array( $value ) ) {
// Filter out empty values to improve data quality.
return array_filter( array_map( 'sanitize_text_field', $value ) );
}
// Try to decode JSON array.
$decoded = json_decode( $value, true );
if ( is_array( $decoded ) ) {
// Filter out empty values to improve data quality.
return wp_json_encode( array_filter( array_map( 'sanitize_text_field', $decoded ) ) );
}
// Extended condition types support.
$is_custom_field = ( 'custom' === ( $condition['rule'] ?? '' ) ) &&
in_array( $condition['type'] ?? '', [ 'post_meta', 'user_meta', 'query_arg', 'referrer', 'cookie', 'options' ], true );
if ( $is_custom_field && false !== strpos( $value, '|' ) ) {
$parts = explode( '|', $value, 2 );
$field_name = sanitize_text_field( $parts[0] );
$comparison_value = sanitize_text_field( $parts[1] ?? '' );
// Rebuild the value with sanitized parts.
return $field_name . '|' . $comparison_value;
}
return sanitize_text_field( $value );
}
/**
* Get all available condition types.
*
* @return array
*/
public static function get_condition_types() {
// Ensure conditions are registered if called early.
if ( empty( GenerateBlocks_Pro_Conditions_Registry::get_all() ) && did_action( 'init' ) ) {
self::get_instance()->register_core_conditions();
}
$types = GenerateBlocks_Pro_Conditions_Registry::get_all();
// Format for backward compatibility.
$formatted = [];
foreach ( $types as $key => $type ) {
$formatted[ $key ] = [
'label' => $type['label'],
'class' => $type['class'],
'operators' => $type['operators'],
];
}
return apply_filters( 'generateblocks_condition_types', $formatted );
}
/**
* Get rules for a specific condition type.
*
* @param string $type The condition type.
* @return array
*/
public static function get_condition_rules( $type = '' ) {
// Ensure conditions are registered if called early.
if ( empty( GenerateBlocks_Pro_Conditions_Registry::get_all() ) && did_action( 'init' ) ) {
self::get_instance()->register_core_conditions();
}
$instance = GenerateBlocks_Pro_Conditions_Registry::get_instance( $type );
if ( ! $instance ) {
return [];
}
$rules = $instance->get_rules();
return apply_filters( 'generateblocks_condition_rules', $rules, $type );
}
/**
* Get location rules (from your existing system).
*
* @return array
*/
private static function get_location_rules() {
$rules = [
'general:site' => __( 'Entire Site', 'generateblocks-pro' ),
'general:front_page' => __( 'Front Page', 'generateblocks-pro' ),
'general:blog' => __( 'Blog', 'generateblocks-pro' ),
'general:singular' => __( 'All Singular', 'generateblocks-pro' ),
'general:archive' => __( 'All Archives', 'generateblocks-pro' ),
'general:author' => __( 'Author Archives', 'generateblocks-pro' ),
'general:date' => __( 'Date Archives', 'generateblocks-pro' ),
'general:search' => __( 'Search Results', 'generateblocks-pro' ),
'general:404' => __( '404 Template', 'generateblocks-pro' ),
];
// Add post types.
$post_types = get_post_types( [ 'public' => true ], 'objects' );
foreach ( $post_types as $post_type_slug => $post_type ) {
$rules[ 'post:' . $post_type_slug ] = $post_type->labels->singular_name;
if ( $post_type->has_archive ) {
// translators: %s is the singular name of the post type.
$rules[ 'archive:' . $post_type_slug ] = sprintf( __( '%s Archive', 'generateblocks-pro' ), $post_type->labels->singular_name );
}
}
return $rules;
}
/**
* Get user role rules.
*
* @return array
*/
private static function get_user_role_rules() {
$rules = [
'general:all' => __( 'All Users', 'generateblocks-pro' ),
'general:logged_in' => __( 'Logged In', 'generateblocks-pro' ),
'general:logged_out' => __( 'Logged Out', 'generateblocks-pro' ),
];
if ( function_exists( 'get_editable_roles' ) ) {
$roles = get_editable_roles();
foreach ( $roles as $slug => $data ) {
$rules[ $slug ] = $data['name'];
}
}
return $rules;
}
/**
* Get metadata for a specific rule.
*
* @param string $type The condition type.
* @param string $rule The rule key.
* @return array
*/
public static function get_rule_metadata( $type, $rule ) {
// Ensure conditions are registered if called early.
if ( empty( GenerateBlocks_Pro_Conditions_Registry::get_all() ) && did_action( 'init' ) ) {
self::get_instance()->register_core_conditions();
}
$instance = GenerateBlocks_Pro_Conditions_Registry::get_instance( $type );
if ( ! $instance ) {
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => false,
];
}
$metadata = $instance->get_rule_metadata( $rule );
return apply_filters( 'generateblocks_rule_metadata', $metadata, $type, $rule );
}
/**
* Main function to determine if conditions are met.
*
* @param array $conditions The conditions array.
* @param array $context Optional context data (e.g., post_id for loop context).
* @return bool
*/
public static function show( $conditions, $context = [] ) {
if ( empty( $conditions ) || empty( $conditions['groups'] ) ) {
return true;
}
$group_results = [];
foreach ( $conditions['groups'] as $group ) {
if ( empty( $group['conditions'] ) ) {
$group_results[] = true;
continue;
}
$condition_results = [];
foreach ( $group['conditions'] as $condition ) {
$condition_results[] = self::evaluate_single_condition( $condition, $context );
}
// Apply group logic (AND/OR within group).
if ( 'OR' === $group['logic'] ) {
$group_results[] = in_array( true, $condition_results, true );
} else {
$group_results[] = ! in_array( false, $condition_results, true );
}
}
// Apply top-level logic to combine group results.
$top_logic = $conditions['logic'] ?? 'OR';
if ( 'AND' === $top_logic ) {
return ! in_array( false, $group_results, true );
} else {
return in_array( true, $group_results, true );
}
}
/**
* Evaluate a single condition.
*
* @param array $condition The condition to evaluate.
* @param array $context Optional context data (e.g., post_id for loop context).
* @return bool
*/
private static function evaluate_single_condition( $condition, $context = [] ) {
// Ensure conditions are registered.
if ( empty( GenerateBlocks_Pro_Conditions_Registry::get_all() ) && did_action( 'init' ) ) {
self::get_instance()->register_core_conditions();
}
if ( empty( $condition['type'] ) || empty( $condition['rule'] ) || empty( $condition['operator'] ) ) {
return false;
}
return GenerateBlocks_Pro_Conditions_Registry::evaluate(
$condition['type'],
$condition['rule'],
$condition['operator'],
$condition['value'] ?? '',
$context // Pass context through to conditions.
);
}
}
// Initialize the singleton instance.
GenerateBlocks_Pro_Conditions::get_instance()->init();
@@ -0,0 +1,41 @@
<?php
/**
* Conditions.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Core files - order matters!
require_once 'interface-condition.php';
require_once 'class-condition-abstract.php';
require_once 'class-conditions-registry.php';
// Individual condition types.
require_once 'conditions/class-condition-location.php';
require_once 'conditions/class-condition-query-arg.php';
require_once 'conditions/class-condition-user-role.php';
require_once 'conditions/class-condition-date-time.php';
require_once 'conditions/class-condition-device.php';
require_once 'conditions/class-condition-referrer.php';
require_once 'conditions/class-condition-post-meta.php';
require_once 'conditions/class-condition-user-meta.php';
require_once 'conditions/class-condition-cookie.php';
require_once 'conditions/class-condition-language.php';
require_once 'conditions/class-condition-options.php';
require_once 'conditions/class-condition-author.php';
// Main conditions class.
require_once 'class-conditions.php';
// REST API.
require_once 'class-conditions-rest.php';
// Post type registration.
require_once 'class-conditions-post-type.php';
// Admin dashboard.
require_once 'class-conditions-dashboard.php';
@@ -0,0 +1,245 @@
<?php
/**
* Author Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Author condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Author extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
$author_id = null;
// Use context if provided, otherwise get current post.
$post_id = ! empty( $context['post_id'] ) ? $context['post_id'] : get_the_ID();
if ( $post_id ) {
$post = get_post( $post_id );
if ( $post ) {
$author_id = $post->post_author;
}
} elseif ( is_author() ) {
// Special case for author archive pages.
$author_id = get_queried_object_id();
}
if ( ! $author_id ) {
return false;
}
// Handle author meta custom fields.
if ( 'author_meta' === $rule ) {
$parsed = $this->parse_meta_field( $rule, $value );
$meta_key = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $meta_key ) ) {
return false;
}
// Handle existence operators using standardized method.
if ( in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return $this->evaluate_meta_existence( 'user', $author_id, $meta_key, $operator );
}
// Handle other operators using standardized method.
return $this->evaluate_meta_value( 'user', $author_id, $meta_key, $operator, $comparison_value );
}
// Handle multi-value operators for author selection.
if ( $this->is_multi_value_operator( $operator ) ) {
return $this->evaluate_multi_value_generic(
$operator,
$value,
function( $check_value ) use ( $rule, $author_id ) {
return $this->check_single_author_match( $rule, $check_value, $author_id );
}
);
}
$is_match = false;
switch ( $rule ) {
case 'author_name':
// Check author display name or login.
if ( empty( $value ) ) {
return false;
}
$author = get_userdata( $author_id );
if ( $author ) {
$is_match = ( $author->display_name === $value ) || ( $author->user_login === $value );
}
break;
case 'author_id':
if ( empty( $value ) || '' === $value ) {
return false;
}
// Check specific author ID.
$is_match = ( $author_id == $value ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
break;
case 'first_name':
case 'last_name':
case 'nickname':
case 'description':
// Check specific user meta fields using standardized method.
if ( in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return $this->evaluate_meta_existence( 'user', $author_id, $rule, $operator );
}
return $this->evaluate_meta_value( 'user', $author_id, $rule, $operator, $value );
}
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Check if a single author value matches the current author.
*
* @param string $rule The condition rule.
* @param mixed $value The value to check.
* @param int $author_id Current author ID.
* @return bool
*/
private function check_single_author_match( $rule, $value, $author_id ) {
switch ( $rule ) {
case 'author_id':
if ( empty( $value ) || '' === $value ) {
return false;
}
return ( $author_id == $value ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
case 'author_name':
if ( empty( $value ) || '' === $value ) {
return false;
}
$author = get_userdata( $author_id );
if ( $author ) {
return ( $author->display_name === $value ) || ( $author->user_login === $value );
}
break;
}
return false;
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'author_id' => __( 'Author ID', 'generateblocks-pro' ),
'author_name' => __( 'Author Name', 'generateblocks-pro' ),
'author_meta' => __( 'Author Meta Key', 'generateblocks-pro' ),
];
// Add common author meta fields.
$author_fields = [
'first_name' => __( 'First Name', 'generateblocks-pro' ),
'last_name' => __( 'Last Name', 'generateblocks-pro' ),
'nickname' => __( 'Nickname', 'generateblocks-pro' ),
'description' => __( 'Biographical Info', 'generateblocks-pro' ),
];
$rules = array_merge( $rules, $author_fields );
return apply_filters( 'generateblocks_author_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'author_meta' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
'supports_multi' => true,
];
}
if ( 'author_id' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'object_selector',
'supports_multi' => true,
];
}
// Text fields don't need greater_than/less_than.
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => false,
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'author_meta' === $rule ) {
return $this->sanitize_custom_value( $value );
}
// Handle array values for multi-select.
if ( is_array( $value ) ) {
return array_map( 'sanitize_text_field', $value );
}
return sanitize_text_field( $value );
}
/**
* Get operators available for a specific author rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// Author meta supports all operators.
if ( 'author_meta' === $rule ) {
return [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
}
// Author ID supports multi-select.
if ( 'author_id' === $rule ) {
return [ 'is', 'is_not', 'includes_any', 'excludes_any' ];
}
// Text fields - no greater_than/less_than.
if ( in_array( $rule, [ 'author_name', 'first_name', 'last_name', 'nickname', 'description' ], true ) ) {
return [ 'exists', 'not_exists', 'equals', 'contains', 'not_contains' ];
}
return [ 'is', 'is_not' ];
}
}
@@ -0,0 +1,153 @@
<?php
/**
* Cookie Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Cookie condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Cookie extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Parse the cookie name and comparison value using standardized method.
$parsed = $this->parse_meta_field( $rule, $value );
$cookie_name = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $cookie_name ) ) {
return false;
}
switch ( $operator ) {
case 'exists':
return $this->cookie_exists( $cookie_name );
case 'not_exists':
return ! $this->cookie_exists( $cookie_name );
case 'has_value':
return $this->cookie_has_value( $cookie_name );
case 'no_value':
return ! $this->cookie_has_value( $cookie_name );
case 'equals':
$cookie_value = $this->get_cookie_value( $cookie_name );
return null !== $cookie_value && $cookie_value === $comparison_value;
case 'contains':
$cookie_value = $this->get_cookie_value( $cookie_name );
return null !== $cookie_value && false !== strpos( $cookie_value, $comparison_value );
case 'not_contains':
$cookie_value = $this->get_cookie_value( $cookie_name );
return null === $cookie_value || false === strpos( $cookie_value, $comparison_value );
case 'starts_with':
$cookie_value = $this->get_cookie_value( $cookie_name );
return null !== $cookie_value && 0 === strpos( $cookie_value, $comparison_value );
case 'ends_with':
$cookie_value = $this->get_cookie_value( $cookie_name );
return null !== $cookie_value && substr( $cookie_value, -strlen( $comparison_value ) ) === $comparison_value;
default:
return false;
}
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'custom' => __( 'Custom Cookie', 'generateblocks-pro' ),
];
// Add common cookie rules that might be useful.
$common_cookies = [
'wordpress_logged_in' => __( 'WordPress Logged In Cookie', 'generateblocks-pro' ),
'comment_author' => __( 'Comment Author Cookie', 'generateblocks-pro' ),
];
$rules = array_merge( $rules, $common_cookies );
// Allow filtering to add predefined cookies.
return apply_filters( 'generateblocks_cookie_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
];
}
// WordPress cookies typically need existence checks only.
if ( in_array( $rule, [ 'wordpress_logged_in', 'comment_author' ], true ) ) {
return [
'needs_value' => false,
'value_type' => 'none',
];
}
return [
'needs_value' => true,
'value_type' => 'text',
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'custom' === $rule ) {
return $this->sanitize_custom_value( $value );
}
return sanitize_text_field( $value );
}
/**
* Get operators available for a specific cookie rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// WordPress cookies typically only need existence checks.
if ( in_array( $rule, [ 'wordpress_logged_in', 'comment_author' ], true ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value' ];
}
// Custom cookies support all text operators including not_contains.
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'starts_with', 'ends_with' ];
}
}
@@ -0,0 +1,399 @@
<?php
/**
* Date Time Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Date/time condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Date_Time extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Handle day of week separately with multi-select support.
if ( 'day_of_week' === $rule ) {
return $this->evaluate_day_of_week( $operator, $value );
}
// Get current time as DateTime object for proper timezone handling.
$current_datetime = current_datetime();
if ( empty( $value ) ) {
return false;
}
// Handle time-only rules (current_time and time_of_day).
if ( in_array( $rule, [ 'current_time', 'time_of_day' ], true ) ) {
return $this->evaluate_time_only( $current_datetime, $operator, $value );
}
// For date-based rules, use full timestamp comparison.
$current_time = $current_datetime->getTimestamp();
switch ( $operator ) {
case 'before':
return $this->is_before( $current_time, $value );
case 'after':
return $this->is_after( $current_time, $value );
case 'on':
return $this->is_on( $current_time, $value, $rule );
case 'between':
return $this->is_between( $current_time, $value );
default:
return false;
}
}
/**
* Evaluate time-only condition (ignoring date).
*
* @param DateTime $current_datetime Current DateTime object.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @return bool
*/
private function evaluate_time_only( $current_datetime, $operator, $value ) {
// Convert current time to seconds since midnight.
$current_hours = (int) $current_datetime->format( 'H' );
$current_minutes = (int) $current_datetime->format( 'i' );
$current_seconds_component = (int) $current_datetime->format( 's' );
$current_seconds = ( $current_hours * HOUR_IN_SECONDS ) +
( $current_minutes * MINUTE_IN_SECONDS ) +
$current_seconds_component;
switch ( $operator ) {
case 'before':
$target_seconds = $this->parse_time_to_seconds( $value );
return false !== $target_seconds && $current_seconds < $target_seconds;
case 'after':
$target_seconds = $this->parse_time_to_seconds( $value );
return false !== $target_seconds && $current_seconds > $target_seconds;
case 'on':
// For time "on", check if within the same hour.
$target_seconds = $this->parse_time_to_seconds( $value );
if ( false === $target_seconds ) {
return false;
}
$target_hour = intval( $target_seconds / HOUR_IN_SECONDS );
return $current_hours === $target_hour;
case 'between':
// Handle time range (e.g., "09:00, 17:00" or "22:00, 02:00" for overnight).
$times = array_map( 'trim', explode( ',', $value ) );
if ( 2 !== count( $times ) ) {
return false;
}
$start_seconds = $this->parse_time_to_seconds( $times[0] );
$end_seconds = $this->parse_time_to_seconds( $times[1] );
if ( false === $start_seconds || false === $end_seconds ) {
return false;
}
// Handle overnight ranges (e.g., 22:00 to 02:00).
if ( $start_seconds > $end_seconds ) {
// Current time is either after start OR before end.
return $current_seconds >= $start_seconds || $current_seconds <= $end_seconds;
} else {
// Normal range within same day.
return $current_seconds >= $start_seconds && $current_seconds <= $end_seconds;
}
default:
return false;
}
}
/**
* Parse time string to seconds since midnight.
*
* @param string $time_str Time string (HH:MM or full datetime).
* @return int|false Seconds since midnight or false on failure.
*/
private function parse_time_to_seconds( $time_str ) {
if ( empty( $time_str ) ) {
return false;
}
// For time-only values (HH:MM format), parse directly in WordPress timezone
// to avoid timezone conversion issues.
try {
// Create DateTime in WordPress timezone directly.
$datetime = new DateTime( $time_str, wp_timezone() );
} catch ( Exception $e ) {
// If parsing fails, try with today's date prepended.
try {
$today = current_datetime()->format( 'Y-m-d' );
$datetime = new DateTime( $today . ' ' . $time_str, wp_timezone() );
} catch ( Exception $e2 ) {
return false;
}
}
// Extract time components.
$hours = (int) $datetime->format( 'H' );
$minutes = (int) $datetime->format( 'i' );
$seconds = (int) $datetime->format( 's' );
// Calculate seconds since midnight.
$result = ( $hours * HOUR_IN_SECONDS ) + ( $minutes * MINUTE_IN_SECONDS ) + $seconds;
return $result;
}
/**
* Evaluate day of week condition.
*
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @return bool
*/
private function evaluate_day_of_week( $operator, $value ) {
// Get current day of week (1 = Monday, 7 = Sunday).
$current_day = intval( current_datetime()->format( 'N' ) );
// Handle multi-value operators.
if ( $this->is_multi_value_operator( $operator ) ) {
return $this->evaluate_multi_value( $operator, $current_day, $value );
}
// Parse value for single operators.
$target_day = intval( $value );
switch ( $operator ) {
case 'is':
return $current_day === $target_day;
case 'is_not':
return $current_day !== $target_day;
default:
return false;
}
}
/**
* Check if current time is before target time.
*
* @param int $current_time Current timestamp.
* @param string $target_time Target time string.
* @return bool
*/
private function is_before( $current_time, $target_time ) {
try {
// Parse the target time using WordPress timezone.
$target_datetime = new DateTime( $target_time, wp_timezone() );
$compare_time = $target_datetime->getTimestamp();
return $current_time < $compare_time;
} catch ( Exception $e ) {
return false;
}
}
/**
* Check if current time is after target time.
*
* @param int $current_time Current timestamp.
* @param string $target_time Target time string.
* @return bool
*/
private function is_after( $current_time, $target_time ) {
try {
// Parse the target time using WordPress timezone.
$target_datetime = new DateTime( $target_time, wp_timezone() );
$compare_time = $target_datetime->getTimestamp();
return $current_time > $compare_time;
} catch ( Exception $e ) {
return false;
}
}
/**
* Check if current time is on target date.
*
* @param int $current_time Current timestamp.
* @param string $target_time Target time string.
* @param string $rule The rule type.
* @return bool
*/
private function is_on( $current_time, $target_time, $rule ) {
try {
// Parse the target time using WordPress timezone.
$target_datetime = new DateTime( $target_time, wp_timezone() );
$current_datetime = new DateTime();
$current_datetime->setTimestamp( $current_time );
$current_datetime->setTimezone( wp_timezone() );
if ( 'current_date' === $rule ) {
// Compare dates in WordPress timezone.
return $current_datetime->format( 'Y-m-d' ) === $target_datetime->format( 'Y-m-d' );
}
// For time comparison, check if within the same hour.
return $current_datetime->format( 'Y-m-d H' ) === $target_datetime->format( 'Y-m-d H' );
} catch ( Exception $e ) {
return false;
}
}
/**
* Check if current time is between two times.
*
* @param int $current_time Current timestamp.
* @param string $value Comma-separated start and end times.
* @return bool
*/
private function is_between( $current_time, $value ) {
$dates = array_map( 'trim', explode( ',', $value ) );
if ( 2 !== count( $dates ) ) {
return false;
}
try {
// Parse both dates using WordPress timezone.
$start_datetime = new DateTime( $dates[0], wp_timezone() );
$end_datetime = new DateTime( $dates[1], wp_timezone() );
$start_time = $start_datetime->getTimestamp();
$end_time = $end_datetime->getTimestamp();
return $current_time >= $start_time && $current_time <= $end_time;
} catch ( Exception $e ) {
return false;
}
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'current_date' => __( 'Date', 'generateblocks-pro' ),
'current_time' => __( 'Time', 'generateblocks-pro' ),
'day_of_week' => __( 'Day of Week', 'generateblocks-pro' ),
'time_of_day' => __( 'Time of Day', 'generateblocks-pro' ), // Hidden in UI, kept for backward compatibility.
];
return apply_filters( 'generateblocks_date_time_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'day_of_week' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'day_selector',
'supports_multi' => true,
];
}
// Time-only rules use time picker.
if ( in_array( $rule, [ 'current_time', 'time_of_day' ], true ) ) {
return [
'needs_value' => true,
'value_type' => 'time',
];
}
// Date rule uses datetime picker.
return [
'needs_value' => true,
'value_type' => 'datetime',
];
}
/**
* Get operators available for a specific date/time rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
if ( 'day_of_week' === $rule ) {
return [ 'is', 'is_not', 'includes_any', 'excludes_any' ];
}
// Other date/time rules use temporal operators.
return [ 'before', 'after', 'between', 'on' ];
}
/**
* Get available day options.
*
* @return array
*/
public function get_day_options() {
return [
'1' => __( 'Monday', 'generateblocks-pro' ),
'2' => __( 'Tuesday', 'generateblocks-pro' ),
'3' => __( 'Wednesday', 'generateblocks-pro' ),
'4' => __( 'Thursday', 'generateblocks-pro' ),
'5' => __( 'Friday', 'generateblocks-pro' ),
'6' => __( 'Saturday', 'generateblocks-pro' ),
'7' => __( 'Sunday', 'generateblocks-pro' ),
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'day_of_week' === $rule ) {
// Handle array values for multi-select.
if ( is_array( $value ) ) {
return array_map( 'intval', $value );
}
// Try to decode JSON array.
$decoded = json_decode( $value, true );
if ( is_array( $decoded ) ) {
return wp_json_encode( array_map( 'intval', $decoded ) );
}
// Single value.
return intval( $value );
}
// For between operator, ensure proper format.
if ( false !== strpos( $value, ',' ) ) {
$dates = array_map( 'trim', explode( ',', $value ) );
$sanitized_dates = array_map( 'sanitize_text_field', $dates );
return implode( ', ', $sanitized_dates );
}
return sanitize_text_field( $value );
}
}
@@ -0,0 +1,116 @@
<?php
/**
* Device Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Device condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Device extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
$is_match = false;
if ( ! function_exists( 'wp_is_mobile' ) ) {
return false;
}
switch ( $rule ) {
case 'mobile':
$is_match = wp_is_mobile() && ! $this->is_tablet();
break;
case 'tablet':
$is_match = $this->is_tablet();
break;
case 'desktop':
$is_match = ! wp_is_mobile();
break;
case 'ios':
$is_match = $this->is_ios();
break;
case 'android':
$is_match = $this->is_android();
break;
}
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Basic tablet detection.
*
* @return bool
*/
private function is_tablet() {
$user_agent = $this->get_server_var( 'HTTP_USER_AGENT' );
return (bool) preg_match( '/(tablet|ipad|playbook|silk)|(android(?!.*mobile))/i', $user_agent );
}
/**
* Check if device is iOS.
*
* @return bool
*/
private function is_ios() {
$user_agent = $this->get_server_var( 'HTTP_USER_AGENT' );
return (bool) preg_match( '/(iPhone|iPod|iPad)/i', $user_agent );
}
/**
* Check if device is Android.
*
* @return bool
*/
private function is_android() {
$user_agent = $this->get_server_var( 'HTTP_USER_AGENT' );
return (bool) preg_match( '/Android/i', $user_agent );
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'mobile' => __( 'Mobile', 'generateblocks-pro' ),
'tablet' => __( 'Tablet', 'generateblocks-pro' ),
'desktop' => __( 'Desktop', 'generateblocks-pro' ),
'ios' => __( 'iOS', 'generateblocks-pro' ),
'android' => __( 'Android', 'generateblocks-pro' ),
];
return apply_filters( 'generateblocks_device_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
];
}
}
@@ -0,0 +1,199 @@
<?php
/**
* Language Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Language condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Language extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
$current_locale = $this->get_current_locale();
$is_match = false;
switch ( $rule ) {
case 'locale':
// Full locale match (e.g., en_US).
$is_match = $current_locale === $value;
break;
case 'language':
// Language code match (e.g., en).
$current_lang = substr( $current_locale, 0, 2 );
$is_match = $current_lang === $value;
break;
case 'rtl':
// Check if Right-to-Left language.
$is_match = is_rtl();
break;
case 'custom':
// Custom locale check.
$is_match = $current_locale === $value;
break;
default:
// For predefined locales (en_US, fr_FR, etc.), check directly.
$is_match = $current_locale === $rule;
break;
}
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Get the current locale.
*
* @return string
*/
private function get_current_locale() {
// Check for WPML.
if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
global $sitepress;
if ( $sitepress && method_exists( $sitepress, 'get_locale' ) ) {
$locale = $sitepress->get_locale( ICL_LANGUAGE_CODE );
if ( $locale ) {
return $locale;
}
}
}
// Check for Polylang.
if ( function_exists( 'pll_current_language' ) ) {
$locale = pll_current_language( 'locale' );
if ( $locale ) {
return $locale;
}
}
// Default WordPress locale.
return get_locale();
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'locale' => __( 'Full Locale (e.g., en_US)', 'generateblocks-pro' ),
'language' => __( 'Language Code (e.g., en)', 'generateblocks-pro' ),
'rtl' => __( 'RTL Language', 'generateblocks-pro' ),
'custom' => __( 'Custom Locale', 'generateblocks-pro' ),
];
// Add common locales.
$common_locales = [
'en_US' => __( 'English (United States)', 'generateblocks-pro' ),
'en_GB' => __( 'English (UK)', 'generateblocks-pro' ),
'en_CA' => __( 'English (Canada)', 'generateblocks-pro' ),
'en_AU' => __( 'English (Australia)', 'generateblocks-pro' ),
'es_ES' => __( 'Spanish (Spain)', 'generateblocks-pro' ),
'es_MX' => __( 'Spanish (Mexico)', 'generateblocks-pro' ),
'fr_FR' => __( 'French (France)', 'generateblocks-pro' ),
'fr_CA' => __( 'French (Canada)', 'generateblocks-pro' ),
'de_DE' => __( 'German', 'generateblocks-pro' ),
'it_IT' => __( 'Italian', 'generateblocks-pro' ),
'pt_BR' => __( 'Portuguese (Brazil)', 'generateblocks-pro' ),
'pt_PT' => __( 'Portuguese (Portugal)', 'generateblocks-pro' ),
'nl_NL' => __( 'Dutch', 'generateblocks-pro' ),
'ru_RU' => __( 'Russian', 'generateblocks-pro' ),
'ja' => __( 'Japanese', 'generateblocks-pro' ),
'zh_CN' => __( 'Chinese (Simplified)', 'generateblocks-pro' ),
'zh_TW' => __( 'Chinese (Traditional)', 'generateblocks-pro' ),
'ko_KR' => __( 'Korean', 'generateblocks-pro' ),
'ar' => __( 'Arabic', 'generateblocks-pro' ),
'he_IL' => __( 'Hebrew', 'generateblocks-pro' ),
];
// Get installed languages if available.
if ( function_exists( 'get_available_languages' ) ) {
$installed = get_available_languages();
foreach ( $installed as $locale ) {
if ( ! isset( $common_locales[ $locale ] ) && 'en_US' !== $locale ) {
$common_locales[ $locale ] = $locale;
}
}
}
$rules = array_merge( $rules, $common_locales );
return apply_filters( 'generateblocks_language_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
// RTL check doesn't need a value.
if ( 'rtl' === $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
];
}
// Language code needs a 2-letter code.
if ( 'language' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'text',
];
}
// Custom and locale need full locale codes.
if ( in_array( $rule, [ 'locale', 'custom' ], true ) ) {
return [
'needs_value' => true,
'value_type' => 'text',
];
}
// Predefined locales don't need values.
return [
'needs_value' => false,
'value_type' => 'none',
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
// For language codes, ensure lowercase and 2 characters.
if ( 'language' === $rule ) {
return strtolower( substr( sanitize_text_field( $value ), 0, 2 ) );
}
// For locales, allow underscores and dashes.
if ( in_array( $rule, [ 'locale', 'custom' ], true ) ) {
return preg_replace( '/[^a-zA-Z0-9_-]/', '', sanitize_text_field( $value ) );
}
return sanitize_text_field( $value );
}
}
@@ -0,0 +1,537 @@
<?php
/**
* Location Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Location condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Location extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
$current_location = $this->get_current_location();
// Handle parent/child relationships first (they have their own multi-value handling).
if ( in_array( $rule, [ 'child_of', 'parent_of' ], true ) ) {
return $this->evaluate_hierarchy( $rule, $operator, $value, $context );
}
// Handle post taxonomy terms.
if ( 0 === strpos( $rule, 'post_terms:' ) ) {
return $this->evaluate_post_terms( $rule, $operator, $value, $context );
}
// Handle multi-value operators for non-hierarchical rules.
if ( $this->is_multi_value_operator( $operator ) ) {
return $this->evaluate_multi_value_generic(
$operator,
$value,
function( $check_value ) use ( $rule, $current_location ) {
return $this->check_single_location_match( $rule, $check_value, $current_location );
}
);
}
$is_match = false;
// Check if current location matches the rule.
if ( in_array( $rule, $current_location['location'], true ) ) {
if ( empty( $value ) ) {
$is_match = true;
} else {
// Check specific object (post ID, term ID, etc.).
$is_match = ( $current_location['object'] == $value ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
}
}
// Special cases - use true original query to check page context, not loop context.
if ( ! $is_match ) {
global $wp_the_query;
switch ( $rule ) {
case 'general:site':
$is_match = true;
break;
case 'general:singular':
$is_match = $wp_the_query->is_singular ?? is_singular();
break;
case 'general:archive':
$is_match = $wp_the_query->is_archive ?? is_archive();
break;
}
}
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Check if a single location value matches the current location.
*
* @param string $rule The condition rule.
* @param mixed $check_value The value to check.
* @param array $current_location Current location data.
* @return bool
*/
private function check_single_location_match( $rule, $check_value, $current_location ) {
// Check if current location rule matches.
if ( in_array( $rule, $current_location['location'], true ) ) {
if ( empty( $check_value ) ) {
return true;
} else {
// Check specific object (post ID, term ID, etc.).
return ( $current_location['object'] == $check_value ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
}
}
// Special cases for general rules - use true original query.
global $wp_the_query;
switch ( $rule ) {
case 'general:site':
return true;
case 'general:singular':
return $wp_the_query->is_singular ?? is_singular();
case 'general:archive':
return $wp_the_query->is_archive ?? is_archive();
}
return false;
}
/**
* Evaluate post taxonomy terms condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
private function evaluate_post_terms( $rule, $operator, $value, $context = [] ) {
// Use context if provided, otherwise get current post ID.
$post_id = ! empty( $context['post_id'] ) ? $context['post_id'] : get_the_ID();
if ( ! $post_id ) {
return false;
}
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Extract taxonomy from rule.
$taxonomy = str_replace( 'post_terms:', '', $rule );
if ( empty( $taxonomy ) || ! taxonomy_exists( $taxonomy ) ) {
return false;
}
// Get post terms.
$post_terms = get_the_terms( $post->ID, $taxonomy );
if ( is_wp_error( $post_terms ) ) {
return false;
}
// Convert to array of term IDs.
$post_term_ids = [];
if ( $post_terms && is_array( $post_terms ) ) {
$post_term_ids = wp_list_pluck( $post_terms, 'term_id' );
$post_term_ids = array_map( 'strval', $post_term_ids ); // Convert to strings for comparison.
}
// Handle multi-value operators.
if ( $this->is_multi_value_operator( $operator ) ) {
return $this->evaluate_multi_value_array( $operator, $post_term_ids, $value );
}
// For single value operators.
if ( empty( $value ) ) {
// No specific term - just check if post has any terms.
return 'is_not' === $operator ? empty( $post_term_ids ) : ! empty( $post_term_ids );
}
$has_term = in_array( strval( $value ), $post_term_ids, true );
return 'is_not' === $operator ? ! $has_term : $has_term;
}
/**
* Evaluate hierarchical relationships.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
private function evaluate_hierarchy( $rule, $operator, $value, $context = [] ) {
// Use context if provided, otherwise get current post ID.
$post_id = ! empty( $context['post_id'] ) ? $context['post_id'] : get_the_ID();
if ( ! $post_id ) {
return false;
}
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Check if post type is hierarchical.
$post_type_object = get_post_type_object( $post->post_type );
if ( ! $post_type_object || ! $post_type_object->hierarchical ) {
return false;
}
// Handle empty value - check for any parent/child relationship.
if ( empty( $value ) ) {
return $this->check_any_hierarchy( $rule, $operator, $post );
}
// Handle multi-value for hierarchy.
if ( $this->is_multi_value_operator( $operator ) ) {
return $this->evaluate_multi_value_generic(
$operator,
$value,
function( $check_value ) use ( $rule, $post ) {
return $this->check_single_hierarchy( $rule, $check_value, $post );
}
);
}
$is_match = $this->check_single_hierarchy( $rule, $value, $post );
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Check single hierarchy relationship.
*
* @param string $rule The hierarchy rule.
* @param mixed $value The value to check.
* @param WP_Post $post The post object to check.
* @return bool
*/
private function check_single_hierarchy( $rule, $value, $post ) {
if ( 'child_of' === $rule ) {
// Check if current post is a child of the specified post.
$ancestors = get_post_ancestors( $post->ID );
return in_array( intval( $value ), $ancestors, true );
} elseif ( 'parent_of' === $rule ) {
// Check if current post is a parent of the specified post.
$target_post = get_post( intval( $value ) );
if ( $target_post ) {
$target_ancestors = get_post_ancestors( $target_post->ID );
return in_array( $post->ID, $target_ancestors, true );
}
}
return false;
}
/**
* Check if post has any hierarchical relationship.
*
* @param string $rule The hierarchy rule.
* @param string $operator The condition operator.
* @param WP_Post $post The post object to check.
* @return bool
*/
private function check_any_hierarchy( $rule, $operator, $post ) {
if ( 'child_of' === $rule ) {
// Check if current post has any parent.
$has_parent = 0 < $post->post_parent;
return 'is_not' === $operator ? ! $has_parent : $has_parent;
} elseif ( 'parent_of' === $rule ) {
// Check if current post has any children.
$children = get_children(
array(
'post_parent' => $post->ID,
'post_type' => $post->post_type,
'numberposts' => 1,
'post_status' => array( 'publish', 'private', 'draft' ),
)
);
$has_children = ! empty( $children );
return 'is_not' === $operator ? ! $has_children : $has_children;
}
return false;
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'general:site' => __( 'Entire Site', 'generateblocks-pro' ),
'general:front_page' => __( 'Front Page', 'generateblocks-pro' ),
'general:blog' => __( 'Blog', 'generateblocks-pro' ),
'general:singular' => __( 'All Singular', 'generateblocks-pro' ),
'general:archive' => __( 'All Archives', 'generateblocks-pro' ),
'general:author' => __( 'Author Archives', 'generateblocks-pro' ),
'general:date' => __( 'Date Archives', 'generateblocks-pro' ),
'general:search' => __( 'Search Results', 'generateblocks-pro' ),
'general:no_results' => __( 'No Results', 'generateblocks-pro' ),
'general:404' => __( '404 Template', 'generateblocks-pro' ),
];
// Add hierarchical relationships.
$rules['child_of'] = __( 'Child of', 'generateblocks-pro' );
$rules['parent_of'] = __( 'Parent of', 'generateblocks-pro' );
// Add post types.
$post_types = get_post_types( [ 'public' => true ], 'objects' );
foreach ( $post_types as $post_type_slug => $post_type ) {
$rules[ 'post:' . $post_type_slug ] = $post_type->labels->singular_name;
if ( $post_type->has_archive ) {
$rules[ 'archive:' . $post_type_slug ] = sprintf(
/* translators: %s: post type singular name */
__( '%s Archive', 'generateblocks-pro' ),
$post_type->labels->singular_name
);
}
}
// Add taxonomies for archive pages.
$taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) {
$rules[ 'taxonomy:' . $taxonomy_slug ] = sprintf(
/* translators: %s: taxonomy singular name */
__( '%s Archive', 'generateblocks-pro' ),
$taxonomy->labels->singular_name
);
}
// Add post taxonomy terms - for checking if current post has specific terms.
foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) {
// Use name if available, fallback to singular_name for tests.
$taxonomy_name = isset( $taxonomy->labels->name ) ? $taxonomy->labels->name : $taxonomy->labels->singular_name;
$rules[ 'post_terms:' . $taxonomy_slug ] = sprintf(
/* translators: %s: taxonomy name */
__( 'Post %s', 'generateblocks-pro' ),
$taxonomy_name
);
}
return apply_filters( 'generateblocks_location_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
$general_location_rules = array(
'general:site',
'general:front_page',
'general:blog',
'general:singular',
'general:archive',
'general:author',
'general:date',
'general:search',
'general:no_results',
'general:404',
);
if ( in_array( $rule, $general_location_rules, true ) ) {
return array(
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
);
}
// Parent/child relationships need object selection but allow empty values.
if ( in_array( $rule, array( 'child_of', 'parent_of' ), true ) ) {
return array(
'needs_value' => false,
'value_type' => 'hierarchical_object_selector',
'supports_multi' => true,
);
}
// Archive rules typically don't need values.
if ( 0 === strpos( $rule, 'archive:' ) ) {
return array(
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
);
}
// Post taxonomy terms need term selection.
if ( 0 === strpos( $rule, 'post_terms:' ) ) {
return array(
'needs_value' => false,
'value_type' => 'object_selector',
'supports_multi' => true,
);
}
// Post and taxonomy rules can optionally have specific object selection.
if ( 0 === strpos( $rule, 'post:' ) || 0 === strpos( $rule, 'taxonomy:' ) ) {
return array(
'needs_value' => false,
'value_type' => 'object_selector',
'supports_multi' => true,
);
}
return $this->get_default_rule_metadata();
}
/**
* Get current location data.
*
* @return array
*/
private function get_current_location() {
global $wp_the_query;
$location = [];
$object = null;
// Always use the true original query to determine location, not the current loop item.
// This ensures Location conditions check "where you are" not "what you're looking at".
// Using $wp_the_query is bulletproof against query_posts() and other query manipulations.
// Falls back to global functions if $wp_the_query is not available.
if ( $wp_the_query->is_front_page ?? is_front_page() ) {
$location[] = 'general:front_page';
// When "Your homepage displays" is set to "Your latest posts",
// both is_front_page() and is_home() are true.
if ( $wp_the_query->is_home ?? is_home() ) {
$location[] = 'general:blog';
}
if ( $wp_the_query->is_page ?? is_page() ) {
$location[] = 'post:page';
$location[] = 'general:singular';
// Get the front page ID for specific page targeting.
$front_page_id = get_option( 'page_on_front' );
if ( $front_page_id ) {
$object = $front_page_id;
}
}
} elseif ( $wp_the_query->is_home ?? is_home() ) {
$location[] = 'general:blog';
} elseif ( $wp_the_query->is_singular ?? is_singular() ) {
$location[] = 'general:singular';
$queried_object = isset( $wp_the_query ) ? $wp_the_query->get_queried_object() : get_queried_object();
if ( $queried_object && isset( $queried_object->post_type ) ) {
$location[] = 'post:' . $queried_object->post_type;
$object = $queried_object->ID;
}
} elseif ( $wp_the_query->is_archive ?? is_archive() ) {
$location[] = 'general:archive';
if ( ( $wp_the_query->is_category ?? is_category() ) || ( $wp_the_query->is_tag ?? is_tag() ) || ( $wp_the_query->is_tax ?? is_tax() ) ) {
$queried_object = isset( $wp_the_query ) ? $wp_the_query->get_queried_object() : get_queried_object();
if ( $queried_object && isset( $queried_object->taxonomy ) ) {
$location[] = 'taxonomy:' . $queried_object->taxonomy;
$object = $queried_object->term_id;
}
} elseif ( $wp_the_query->is_post_type_archive ?? is_post_type_archive() ) {
$post_type = isset( $wp_the_query ) ? $wp_the_query->get( 'post_type' ) : get_query_var( 'post_type' );
if ( $post_type ) {
$location[] = 'archive:' . $post_type;
}
} elseif ( $wp_the_query->is_author ?? is_author() ) {
$location[] = 'general:author';
} elseif ( $wp_the_query->is_date ?? is_date() ) {
$location[] = 'general:date';
}
} elseif ( $wp_the_query->is_search ?? is_search() ) {
$location[] = 'general:search';
// Check if search has no results.
if ( ( $wp_the_query->found_posts ?? 0 ) === 0 ) {
$location[] = 'general:no_results';
}
} elseif ( $wp_the_query->is_404 ?? is_404() ) {
$location[] = 'general:404';
}
// Always include site.
$location[] = 'general:site';
return [
'location' => $location,
'object' => $object,
];
}
/**
* Get operators available for a specific location rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// General location rules - only basic operators (you're either there or not).
$general_rules = array(
'general:site',
'general:front_page',
'general:blog',
'general:singular',
'general:archive',
'general:author',
'general:date',
'general:search',
'general:no_results',
'general:404',
);
if ( in_array( $rule, $general_rules, true ) ) {
return array( 'is', 'is_not' );
}
// Archive rules - only basic operators.
if ( 0 === strpos( $rule, 'archive:' ) ) {
return array( 'is', 'is_not' );
}
// Hierarchical relationships - simplified operators (no includes_all/excludes_all).
if ( in_array( $rule, array( 'child_of', 'parent_of' ), true ) ) {
return array( 'is', 'is_not', 'includes_any', 'excludes_any' );
}
// Post taxonomy terms - full multi-value support.
if ( 0 === strpos( $rule, 'post_terms:' ) ) {
return array( 'is', 'is_not', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' );
}
// For all other rules, check if multi-select is actually supported.
if ( $this->rule_supports_multi_select( $rule ) ) {
// Multi-select interface available.
if ( 0 === strpos( $rule, 'post:' ) ) {
// Posts: can select multiple but can't be on all simultaneously.
return array( 'is', 'is_not', 'includes_any', 'excludes_any' );
}
// Taxonomies: full support.
return array( 'is', 'is_not', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' );
} else {
// Single input only: no any/all operators.
return array( 'is', 'is_not' );
}
}
}
@@ -0,0 +1,195 @@
<?php
/**
* Options Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Options condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Options extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Parse the option name and comparison value using standardized method.
$parsed = $this->parse_meta_field( $rule, $value );
$option_name = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $option_name ) ) {
return false;
}
// Handle existence operators using standardized method.
if ( in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return $this->evaluate_meta_existence( 'option', 0, $option_name, $operator );
}
// Handle has_value/no_value operators.
if ( 'has_value' === $operator ) {
return $this->evaluate_meta_has_value( 'option', 0, $option_name );
}
if ( 'no_value' === $operator ) {
return ! $this->evaluate_meta_has_value( 'option', 0, $option_name );
}
// Handle all other operators using standardized method.
return $this->evaluate_meta_value( 'option', 0, $option_name, $operator, $comparison_value );
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'custom' => __( 'Custom Option', 'generateblocks-pro' ),
];
// Add common WordPress options.
$common_options = [
'blogname' => __( 'Site Title', 'generateblocks-pro' ),
'blogdescription' => __( 'Site Tagline', 'generateblocks-pro' ),
'timezone_string' => __( 'Timezone', 'generateblocks-pro' ),
'date_format' => __( 'Date Format', 'generateblocks-pro' ),
'time_format' => __( 'Time Format', 'generateblocks-pro' ),
];
$rules = array_merge( $rules, $common_options );
// Allow themes/plugins to add their options.
return apply_filters( 'generateblocks_options_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
'supports_multi' => true,
];
}
// Boolean options need special handling.
$boolean_options = [
'users_can_register',
'comment_registration',
'close_comments_for_old_posts',
'comment_moderation',
'moderation_notify',
'comments_notify',
];
if ( in_array( $rule, $boolean_options, true ) ) {
return [
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
];
}
// Numeric options.
$numeric_options = [
'posts_per_page',
'comments_per_page',
'start_of_week',
];
if ( in_array( $rule, $numeric_options, true ) ) {
return [
'needs_value' => true,
'value_type' => 'number',
'supports_multi' => true,
];
}
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => true,
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'custom' === $rule ) {
return $this->sanitize_custom_value( $value );
}
// Handle array values for multi-select.
if ( is_array( $value ) ) {
return array_map( 'sanitize_text_field', $value );
}
return sanitize_text_field( $value );
}
/**
* Get operators available for a specific option rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// Boolean options only need existence checks.
$boolean_options = [
'users_can_register',
'comment_registration',
'close_comments_for_old_posts',
'comment_moderation',
'moderation_notify',
'comments_notify',
];
if ( in_array( $rule, $boolean_options, true ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value' ];
}
// Numeric options support all operators.
$numeric_options = [
'posts_per_page',
'comments_per_page',
'start_of_week',
];
if ( in_array( $rule, $numeric_options, true ) ) {
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'greater_than', 'less_than', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'greater_than', 'less_than' ];
}
}
// Text options support text operators.
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains' ];
}
}
}
@@ -0,0 +1,154 @@
<?php
/**
* Post Meta Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Post meta condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Post_Meta extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Use context if provided, otherwise get current post ID.
$post_id = ! empty( $context['post_id'] ) ? $context['post_id'] : get_the_ID();
if ( ! $post_id ) {
return false;
}
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Parse the meta key and comparison value using standardized method.
$parsed = $this->parse_meta_field( $rule, $value );
$meta_key = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $meta_key ) ) {
return false;
}
// Handle existence operators using standardized method.
if ( in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return $this->evaluate_meta_existence( 'post', $post->ID, $meta_key, $operator );
}
// Handle has_value/no_value operators.
if ( 'has_value' === $operator ) {
return $this->evaluate_meta_has_value( 'post', $post->ID, $meta_key );
}
if ( 'no_value' === $operator ) {
return ! $this->evaluate_meta_has_value( 'post', $post->ID, $meta_key );
}
// Handle all other operators using standardized method.
return $this->evaluate_meta_value( 'post', $post->ID, $meta_key, $operator, $comparison_value );
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'custom' => __( 'Custom Meta Key', 'generateblocks-pro' ),
];
// Add common meta keys.
$common_keys = [
'_thumbnail_id' => __( 'Has Featured Image', 'generateblocks-pro' ),
'_wp_page_template' => __( 'Page Template', 'generateblocks-pro' ),
];
$rules = array_merge( $rules, $common_keys );
// Allow themes/plugins to add their meta keys.
return apply_filters( 'generateblocks_post_meta_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
'supports_multi' => true,
];
}
// Special handling for specific meta keys.
if ( '_thumbnail_id' === $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
];
}
// Page template is text-based.
if ( '_wp_page_template' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => true,
];
}
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => true,
];
}
/**
* Get operators available for a specific post meta rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// Special case for thumbnail - only existence checks.
if ( '_thumbnail_id' === $rule ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value' ];
}
// Page template - only text operations, no greater_than/less_than.
if ( '_wp_page_template' === $rule ) {
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains' ];
}
}
// Custom meta key supports all operators including numeric.
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than' ];
}
}
}
@@ -0,0 +1,160 @@
<?php
/**
* Query Argument Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Query argument condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Query_Arg extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Parse the parameter name and comparison value using standardized method.
$parsed = $this->parse_meta_field( $rule, $value );
$param_name = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $param_name ) ) {
return false;
}
switch ( $operator ) {
case 'exists':
return $this->query_param_exists( $param_name );
case 'not_exists':
return ! $this->query_param_exists( $param_name );
case 'has_value':
return $this->query_param_has_value( $param_name );
case 'no_value':
return ! $this->query_param_has_value( $param_name );
case 'equals':
$param_value = $this->get_query_param_raw( $param_name );
// For arrays, check if comparison value is in the array.
if ( is_array( $param_value ) ) {
return in_array( $comparison_value, $param_value, true );
}
return null !== $param_value && $param_value === $comparison_value;
case 'contains':
$param_value = $this->get_query_param_raw( $param_name );
// String operations only work on strings, return false for arrays.
if ( ! is_string( $param_value ) ) {
return false;
}
return false !== strpos( $param_value, $comparison_value );
case 'not_contains':
$param_value = $this->get_query_param_raw( $param_name );
// String operations only work on strings, return true for arrays (they don't "contain" the string).
if ( ! is_string( $param_value ) ) {
return true;
}
return false === strpos( $param_value, $comparison_value );
case 'starts_with':
$param_value = $this->get_query_param_raw( $param_name );
// String operations only work on strings, return false for arrays.
if ( ! is_string( $param_value ) ) {
return false;
}
return 0 === strpos( $param_value, $comparison_value );
case 'ends_with':
$param_value = $this->get_query_param_raw( $param_name );
// String operations only work on strings, return false for arrays.
if ( ! is_string( $param_value ) ) {
return false;
}
return substr( $param_value, -strlen( $comparison_value ) ) === $comparison_value;
default:
return false;
}
}
/**
* Get raw query parameter value without sanitization to preserve arrays.
*
* @param string $param_name Parameter name.
* @return mixed|null
*/
private function get_query_param_raw( $param_name ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET[ $param_name ] ) ) {
return null;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$value = wp_unslash( $_GET[ $param_name ] );
// Sanitize based on type.
if ( is_array( $value ) ) {
return array_map( 'sanitize_text_field', $value );
}
return sanitize_text_field( $value );
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'custom' => __( 'Custom Parameter', 'generateblocks-pro' ),
];
// Allow filtering to add predefined parameters.
return apply_filters( 'generateblocks_query_arg_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
];
}
return [
'needs_value' => true,
'value_type' => 'text',
];
}
/**
* Get operators available for a specific query arg rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// All query args support the same text operators.
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'starts_with', 'ends_with' ];
}
}
@@ -0,0 +1,138 @@
<?php
/**
* Referrer Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Referrer condition evaluator.
*/
class GenerateBlocks_Pro_Condition_Referrer extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
$referrer = $this->get_server_var( 'HTTP_REFERER' );
// Handle direct traffic (no referrer).
if ( 'direct' === $rule ) {
$has_referrer = ! empty( $referrer );
return 'is_not' === $operator ? $has_referrer : ! $has_referrer;
}
// Handle custom referrer.
if ( 'custom' === $rule ) {
// Handle has_value/no_value operators first.
if ( 'has_value' === $operator ) {
return $this->referrer_has_value();
}
if ( 'no_value' === $operator ) {
return ! $this->referrer_has_value();
}
$parsed = $this->parse_custom_value( $value );
$comparison_value = $parsed['comparison_value'] ? $parsed['comparison_value'] : $parsed['field_name'];
if ( empty( $comparison_value ) ) {
return false;
}
$is_match = false;
switch ( $operator ) {
case 'contains':
$is_match = false !== strpos( $referrer, $comparison_value );
break;
case 'not_contains':
$is_match = false === strpos( $referrer, $comparison_value );
break;
case 'equals':
$is_match = $referrer === $comparison_value;
break;
case 'starts_with':
$is_match = 0 === strpos( $referrer, $comparison_value );
break;
case 'ends_with':
$is_match = substr( $referrer, -strlen( $comparison_value ) ) === $comparison_value;
break;
}
return $is_match;
}
return false;
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'direct' => __( 'Direct Traffic (No Referrer)', 'generateblocks-pro' ),
'custom' => __( 'Custom Referrer', 'generateblocks-pro' ),
];
return apply_filters( 'generateblocks_referrer_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
];
}
if ( 'direct' === $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
];
}
return $this->get_default_rule_metadata();
}
/**
* Get operators available for a specific referrer rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
if ( 'direct' === $rule ) {
// Direct traffic only supports is/is_not.
return [ 'is', 'is_not' ];
}
if ( 'custom' === $rule ) {
// Custom referrer supports all text comparison operators including not_contains.
return [ 'has_value', 'no_value', 'contains', 'not_contains', 'equals', 'starts_with', 'ends_with' ];
}
return [ 'contains', 'not_contains', 'equals', 'starts_with' ];
}
}
@@ -0,0 +1,166 @@
<?php
/**
* User Meta Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* User meta condition evaluator.
*/
class GenerateBlocks_Pro_Condition_User_Meta extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// User must be logged in for user meta conditions.
if ( ! is_user_logged_in() ) {
return false;
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
return false;
}
// Parse the meta key and comparison value using standardized method.
$parsed = $this->parse_meta_field( $rule, $value );
$meta_key = $parsed['field_name'];
$comparison_value = $parsed['comparison_value'];
if ( empty( $meta_key ) ) {
return false;
}
// Handle existence operators using standardized method.
if ( in_array( $operator, [ 'exists', 'not_exists' ], true ) ) {
return $this->evaluate_meta_existence( 'user', $user_id, $meta_key, $operator );
}
// Handle has_value/no_value operators.
if ( 'has_value' === $operator ) {
return $this->evaluate_meta_has_value( 'user', $user_id, $meta_key );
}
if ( 'no_value' === $operator ) {
return ! $this->evaluate_meta_has_value( 'user', $user_id, $meta_key );
}
// Handle all other operators using standardized method.
return $this->evaluate_meta_value( 'user', $user_id, $meta_key, $operator, $comparison_value );
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'custom' => __( 'Custom Meta Key', 'generateblocks-pro' ),
];
// Add common user meta keys.
$common_keys = [
'first_name' => __( 'First Name', 'generateblocks-pro' ),
'last_name' => __( 'Last Name', 'generateblocks-pro' ),
'nickname' => __( 'Nickname', 'generateblocks-pro' ),
'description' => __( 'Biographical Info', 'generateblocks-pro' ),
'show_admin_bar_front' => __( 'Show Admin Bar', 'generateblocks-pro' ),
];
$rules = array_merge( $rules, $common_keys );
// Allow themes/plugins to add their meta keys.
return apply_filters( 'generateblocks_user_meta_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
if ( 'custom' === $rule ) {
return [
'needs_value' => true,
'value_type' => 'custom_field',
'supports_multi' => true,
];
}
// Show admin bar is a boolean stored as string.
if ( 'show_admin_bar_front' === $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
];
}
return [
'needs_value' => true,
'value_type' => 'text',
'supports_multi' => true,
];
}
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule ) {
if ( 'custom' === $rule ) {
return $this->sanitize_custom_value( $value );
}
// Handle array values for multi-select.
if ( is_array( $value ) ) {
return array_map( 'sanitize_text_field', $value );
}
return sanitize_text_field( $value );
}
/**
* Get operators available for a specific user meta rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// Show admin bar only needs existence checks.
if ( 'show_admin_bar_front' === $rule ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value' ];
}
// Text fields - no greater_than/less_than as per feedback.
if ( in_array( $rule, [ 'first_name', 'last_name', 'nickname', 'description' ], true ) ) {
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains' ];
}
}
// Custom meta key supports all operators.
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'includes_any', 'includes_all', 'excludes_any', 'excludes_all' ];
} else {
return [ 'exists', 'not_exists', 'has_value', 'no_value', 'equals', 'contains', 'not_contains', 'greater_than', 'less_than' ];
}
}
}
@@ -0,0 +1,179 @@
<?php
/**
* User Role Condition
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* User role condition evaluator.
*/
class GenerateBlocks_Pro_Condition_User_Role extends GenerateBlocks_Pro_Condition_Abstract {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] ) {
// Handle multi-value operators for roles and capabilities only.
if ( $this->is_multi_value_operator( $operator ) && $this->rule_supports_multi_select( $rule ) ) {
return $this->evaluate_multi_value_generic(
$operator,
$value,
function( $check_value ) {
return $this->check_single_user_role( $check_value );
}
);
}
$is_match = false;
switch ( $rule ) {
case 'general:logged_in':
$is_match = is_user_logged_in();
break;
case 'general:logged_out':
$is_match = ! is_user_logged_in();
break;
default:
// Check specific role or capability.
$user = wp_get_current_user();
if ( $user && $user->exists() ) {
// Check if it's a capability (prefixed with 'cap:').
if ( 0 === strpos( $rule, 'cap:' ) ) {
// Remove prefix.
$capability = substr( $rule, 4 );
$is_match = user_can( $user, $capability );
} else {
// It's a role.
$is_match = in_array( $rule, (array) $user->roles, true );
}
}
break;
}
return 'is_not' === $operator ? ! $is_match : $is_match;
}
/**
* Check if current user matches a single role/capability.
*
* @param string $check_value The role or capability to check.
* @return bool
*/
private function check_single_user_role( $check_value ) {
$user = wp_get_current_user();
if ( ! $user || ! $user->exists() ) {
// For logged out users, only check against general rules.
return 'general:logged_out' === $check_value;
}
switch ( $check_value ) {
case 'general:logged_in':
return true; // User is logged in if we reach this point.
case 'general:logged_out':
return false; // User is logged in.
default:
// Check specific role or capability.
if ( 0 === strpos( $check_value, 'cap:' ) ) {
// Remove prefix.
$capability = substr( $check_value, 4 );
return user_can( $user, $capability );
} else {
// It's a role.
$user_roles = (array) $user->roles;
return in_array( $check_value, $user_roles, true );
}
}
}
/**
* Get available rules for this condition type.
*
* @return array
*/
public function get_rules() {
$rules = [
'general:logged_in' => __( 'Logged In', 'generateblocks-pro' ),
'general:logged_out' => __( 'Logged Out', 'generateblocks-pro' ),
];
// Ensure get_editable_roles() is available.
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
// Add WordPress roles.
if ( function_exists( 'get_editable_roles' ) ) {
$roles = get_editable_roles();
foreach ( $roles as $slug => $data ) {
$rules[ $slug ] = translate_user_role( $data['name'] );
}
}
// Add capabilities - keep the most commonly used ones.
$capabilities = [
'edit_posts' => __( 'Can Edit Posts', 'generateblocks-pro' ),
'edit_pages' => __( 'Can Edit Pages', 'generateblocks-pro' ),
'edit_published_posts' => __( 'Can Edit Published Posts', 'generateblocks-pro' ),
'edit_others_posts' => __( 'Can Edit Others Posts', 'generateblocks-pro' ),
'publish_posts' => __( 'Can Publish Posts', 'generateblocks-pro' ),
'manage_options' => __( 'Can Manage Options', 'generateblocks-pro' ),
'moderate_comments' => __( 'Can Moderate Comments', 'generateblocks-pro' ),
];
// Capabilities are prefixed with 'cap:' to distinguish from roles.
foreach ( $capabilities as $cap => $label ) {
$rules[ 'cap:' . $cap ] = $label;
}
return apply_filters( 'generateblocks_user_role_rules', $rules );
}
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array
*/
public function get_rule_metadata( $rule ) {
return [
'needs_value' => false,
'value_type' => 'none',
'supports_multi' => false,
];
}
/**
* Get operators available for a specific user role rule.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule ) {
// General status rules - simple is/is_not only.
if ( in_array( $rule, [ 'general:logged_in', 'general:logged_out' ], true ) ) {
return [ 'is', 'is_not' ];
}
// For individual roles and capabilities, support multi-select if UI supports it.
if ( $this->rule_supports_multi_select( $rule ) ) {
return [ 'is', 'is_not', 'includes_any', 'excludes_any' ];
}
// Fallback to simple operators.
return [ 'is', 'is_not' ];
}
}
@@ -0,0 +1,59 @@
<?php
/**
* Condition Interface
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Interface for condition evaluators.
*/
interface GenerateBlocks_Pro_Condition_Interface {
/**
* Evaluate the condition.
*
* @param string $rule The condition rule.
* @param string $operator The condition operator.
* @param mixed $value The value to check against.
* @param array $context Additional context data.
* @return bool
*/
public function evaluate( $rule, $operator, $value, $context = [] );
/**
* Get available rules for this condition type.
*
* @return array Array of rule_key => rule_label pairs.
*/
public function get_rules();
/**
* Get metadata for a specific rule.
*
* @param string $rule The rule key.
* @return array Rule metadata.
*/
public function get_rule_metadata( $rule );
/**
* Get operators available for a specific rule.
* This method should be public since the REST API calls it directly.
*
* @param string $rule The rule key.
* @return array Array of operator keys.
*/
public function get_operators_for_rule( $rule );
/**
* Sanitize the condition value.
*
* @param mixed $value The value to sanitize.
* @param string $rule The rule being used.
* @return mixed
*/
public function sanitize_value( $value, $rule );
}
@@ -0,0 +1,166 @@
<?php
/**
* This file handles our pro option defaults.
*
* @package GenerateBlocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
add_filter( 'generateblocks_defaults', 'generateblocks_pro_set_block_defaults' );
/**
* Set the defaults for our pro options.
*
* @since 1.0
* @param array $defaults The existing defaults.
*/
function generateblocks_pro_set_block_defaults( $defaults ) {
$defaults['container']['backgroundColorHover'] = '';
$defaults['container']['backgroundColorHoverOpacity'] = 1;
$defaults['container']['borderColorHover'] = '';
$defaults['container']['borderColorHoverOpacity'] = 1;
$defaults['container']['textColorHover'] = '';
$defaults['container']['useOpacity'] = false;
$defaults['container']['opacities'] = array();
$defaults['container']['useTransition'] = false;
$defaults['container']['transitions'] = array();
$defaults['container']['useTransform'] = false;
$defaults['container']['transforms'] = array();
$defaults['container']['useFilter'] = false;
$defaults['container']['filters'] = array();
$defaults['container']['useBoxShadow'] = false;
$defaults['container']['boxShadows'] = array();
$defaults['container']['useTextShadow'] = false;
$defaults['container']['textShadows'] = array();
$defaults['container']['useAdvBackgrounds'] = false;
$defaults['container']['advBackgrounds'] = array();
$defaults['container']['linkType'] = 'hidden-link';
$defaults['container']['url'] = '';
$defaults['container']['hiddenLinkAriaLabel'] = '';
$defaults['container']['relNoFollow'] = false;
$defaults['container']['target'] = false;
$defaults['container']['relSponsored'] = false;
$defaults['container']['hideOnDesktop'] = false;
$defaults['container']['hideOnTablet'] = false;
$defaults['container']['hideOnMobile'] = false;
$defaults['container']['useGlobalStyle'] = false;
$defaults['container']['globalStyleId'] = '';
$defaults['container']['backgroundColorCurrent'] = '';
$defaults['container']['textColorCurrent'] = '';
$defaults['container']['borderColorCurrent'] = '';
$defaults['buttonContainer']['hideOnDesktop'] = false;
$defaults['buttonContainer']['hideOnTablet'] = false;
$defaults['buttonContainer']['hideOnMobile'] = false;
$defaults['buttonContainer']['useGlobalStyle'] = false;
$defaults['buttonContainer']['globalStyleId'] = '';
$defaults['button']['useOpacity'] = false;
$defaults['button']['opacities'] = array();
$defaults['button']['useTransition'] = false;
$defaults['button']['transitions'] = array();
$defaults['button']['useTransform'] = false;
$defaults['button']['transforms'] = array();
$defaults['button']['useFilter'] = false;
$defaults['button']['filters'] = array();
$defaults['button']['useBoxShadow'] = false;
$defaults['button']['boxShadows'] = array();
$defaults['button']['useTextShadow'] = false;
$defaults['button']['textShadows'] = array();
$defaults['button']['hideOnDesktop'] = false;
$defaults['button']['hideOnTablet'] = false;
$defaults['button']['hideOnMobile'] = false;
$defaults['button']['useGlobalStyle'] = false;
$defaults['button']['globalStyleId'] = '';
$defaults['headline']['useOpacity'] = false;
$defaults['headline']['opacities'] = array();
$defaults['headline']['useTransition'] = false;
$defaults['headline']['transitions'] = array();
$defaults['headline']['useTransform'] = false;
$defaults['headline']['transforms'] = array();
$defaults['headline']['useFilter'] = false;
$defaults['headline']['filters'] = array();
$defaults['headline']['useBoxShadow'] = false;
$defaults['headline']['boxShadows'] = array();
$defaults['headline']['useTextShadow'] = false;
$defaults['headline']['textShadows'] = array();
$defaults['headline']['hideOnDesktop'] = false;
$defaults['headline']['hideOnTablet'] = false;
$defaults['headline']['hideOnMobile'] = false;
$defaults['headline']['useGlobalStyle'] = false;
$defaults['headline']['globalStyleId'] = '';
$defaults['gridContainer']['hideOnDesktop'] = false;
$defaults['gridContainer']['hideOnTablet'] = false;
$defaults['gridContainer']['hideOnMobile'] = false;
$defaults['gridContainer']['useGlobalStyle'] = false;
$defaults['gridContainer']['globalStyleId'] = '';
$defaults['image']['useOpacity'] = false;
$defaults['image']['opacities'] = array();
$defaults['image']['useTransition'] = false;
$defaults['image']['transitions'] = array();
$defaults['image']['useTransform'] = false;
$defaults['image']['transforms'] = array();
$defaults['image']['useFilter'] = false;
$defaults['image']['filters'] = array();
$defaults['image']['useBoxShadow'] = false;
$defaults['image']['boxShadows'] = array();
$defaults['image']['useTextShadow'] = false;
$defaults['image']['textShadows'] = array();
$defaults['image']['hideOnDesktop'] = false;
$defaults['image']['hideOnTablet'] = false;
$defaults['image']['hideOnMobile'] = false;
return $defaults;
}
/**
* Get defaults for our general options.
*
* @since 0.1
*/
function generateblocks_pro_get_license_defaults() {
return apply_filters(
'generateblocks_license_defaults',
array(
'key' => '',
'status' => '',
'beta' => false,
)
);
}
/**
* Get defaults for our admin options.
* The core plugin doesn't have this yet, so this function is needed for now.
*
* @since 1.1.0
*/
function generateblocks_pro_get_admin_option_defaults() {
// Use the core plugin defaults function if/when it exists.
if ( function_exists( 'generateblocks_get_admin_option_defaults' ) ) {
return generateblocks_get_admin_option_defaults();
}
return apply_filters(
'generateblocks_admin_option_defaults',
array()
);
}
add_filter( 'generateblocks_admin_option_defaults', 'generateblocks_pro_set_admin_option_defaults' );
/**
* Add option defaults.
*
* @param array $defaults Existing defaults.
*/
function generateblocks_pro_set_admin_option_defaults( $defaults ) {
$defaults['enable_remote_templates'] = true;
$defaults['enable_local_templates'] = true;
return $defaults;
}
@@ -0,0 +1,20 @@
<?php
/**
* Deprecated functions.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Check if we need Safe SVG.
*
* @since 1.0.0
* @deprecated 1.1.0
*/
function generateblocks_pro_has_safe_svg() {
return is_plugin_active( 'safe-svg/safe-svg.php' ) || apply_filters( 'generateblocks_override_safe_svg_check', false );
}
@@ -0,0 +1,159 @@
<?php
/**
* Block Conditions functionality.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class GenerateBlocks_Pro_Block_Conditions
*/
class GenerateBlocks_Pro_Block_Conditions extends GenerateBlocks_Pro_Singleton {
/**
* Constructor.
*/
public function init() {
// Don't initialize if block conditions are disabled.
if ( ! generateblocks_pro_block_conditions_enabled() ) {
return;
}
add_filter( 'render_block', [ $this, 'check_block_conditions' ], 10, 2 );
add_filter( 'generateblocks_condition_usage_handlers', [ $this, 'add_block_usage_handler' ], 10, 2 );
add_filter( 'register_block_type_args', [ $this, 'add_condition_attributes' ], 10, 2 );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
}
/**
* Enqueue block editor assets for block conditions.
*/
public function enqueue_editor_assets() {
$assets = generateblocks_pro_get_enqueue_assets( 'block-conditions' );
wp_enqueue_script(
'generateblocks-pro-block-conditions',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/block-conditions.js',
$assets['dependencies'],
$assets['version'],
true
);
}
/**
* Check block conditions and prevent rendering if conditions are not met.
*
* @param string $block_content The block content.
* @param array $block The block data.
* @return string The block content or empty string if conditions are not met.
*/
public function check_block_conditions( $block_content, $block ) {
// Early return if no attributes.
if ( empty( $block['attrs'] ) ) {
return $block_content;
}
$attributes = $block['attrs'];
// Check if this block has a condition set.
if ( empty( $attributes['gbBlockCondition'] ) ) {
return $block_content;
}
$condition_id = absint( $attributes['gbBlockCondition'] );
// Validate condition ID.
if ( ! $condition_id ) {
return $block_content;
}
// Check if the condition post exists and is published.
$condition_post = get_post( $condition_id );
if ( ! $condition_post || 'publish' !== $condition_post->post_status ) {
return $block_content;
}
// Get the condition data.
$display_conditions = get_post_meta( $condition_id, '_gb_conditions', true );
if ( empty( $display_conditions ) ) {
return $block_content;
}
// Pass the current post context to the conditions system.
// This ensures conditions are evaluated for the correct post in loops.
$context = array( 'post_id' => get_the_ID() );
// Use the existing conditions system to evaluate.
$show = GenerateBlocks_Pro_Conditions::show( $display_conditions, $context );
// If invert is enabled, flip the result.
if ( ! empty( $attributes['gbBlockConditionInvert'] ) ) {
$show = ! $show;
}
// Return empty string to prevent block from rendering if condition is not met.
if ( ! $show ) {
return '';
}
return $block_content;
}
/**
* Add block conditions handler to usage search.
*
* @param array $handlers Existing handlers.
* @param int $condition_id The condition ID.
* @return array Modified handlers.
*/
public function add_block_usage_handler( $handlers, $condition_id ) {
$handlers['block_conditions'] = [
'method' => 'search_block_conditions_usage',
'label' => __( 'Block Conditions', 'generateblocks-pro' ),
];
return $handlers;
}
/**
* Add condition attributes to all blocks during server-side registration.
* This ensures ServerSideRender calls don't fail validation.
*
* @param array $args The block registration arguments.
* @param string $block_type The block type name.
* @return array Modified arguments.
*/
public function add_condition_attributes( $args, $block_type ) {
// Ensure attributes array exists.
if ( ! isset( $args['attributes'] ) ) {
$args['attributes'] = [];
}
// Add gbBlockCondition attribute if not already defined.
if ( ! isset( $args['attributes']['gbBlockCondition'] ) ) {
$args['attributes']['gbBlockCondition'] = [
'type' => 'string',
'default' => '',
];
}
// Add gbBlockConditionInvert attribute if not already defined.
if ( ! isset( $args['attributes']['gbBlockConditionInvert'] ) ) {
$args['attributes']['gbBlockConditionInvert'] = [
'type' => 'boolean',
'default' => false,
];
}
return $args;
}
}
// Initialize the class.
GenerateBlocks_Pro_Block_Conditions::get_instance()->init();
@@ -0,0 +1,158 @@
<?php
/**
* The class to integrate advance custom fields to dynamic content post meta.
*
* @package Generateblocks/Extend/DynamicContent
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* GenerateBlocks Pro Advanced custom fields integration
*
* @since 1.4.0
*/
class GenerateBlocks_Pro_Advanced_Custom_Fields extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
protected function __construct() {
parent::__construct();
if ( class_exists( 'ACF' ) ) {
add_filter(
'generateblocks_dynamic_content_post_meta',
'GenerateBlocks_Pro_Advanced_Custom_Fields::load_acf_fields',
10,
3
);
add_filter(
'generateblocks_dynamic_url_post_meta',
'GenerateBlocks_Pro_Advanced_Custom_Fields::load_link_acf_fields',
10,
3
);
add_filter(
'generateblocks_dynamic_content_author_meta',
'GenerateBlocks_Pro_Advanced_Custom_Fields::load_author_acf_fields',
10,
3
);
add_filter(
'generateblocks_dynamic_url_author_meta',
'GenerateBlocks_Pro_Advanced_Custom_Fields::load_author_link_acf_fields',
10,
3
);
}
}
/**
* Load ACF fields in the frontend.
*
* @param mixed $value The field value.
* @param string|int $id The object id.
* @param array $attributes The block attributes.
* @param string $metaFieldKey The meta field key.
* @param string $metaFieldPropertyNameKey The property key.
*
* @return mixed|string The field value.
*/
public static function load_acf_fields(
$value,
$id,
$attributes,
$metaFieldKey = 'metaFieldName',
$metaFieldPropertyNameKey = 'metaFieldPropertyName'
) {
$field = get_field( $attributes[ $metaFieldKey ], $id );
if ( is_array( $field ) ) {
if (
isset( $attributes[ $metaFieldPropertyNameKey ] ) &&
is_string( $attributes[ $metaFieldPropertyNameKey ] ) &&
isset( $field[ $attributes[ $metaFieldPropertyNameKey ] ] )
) {
return $field[ $attributes[ $metaFieldPropertyNameKey ] ];
}
// This is for backwards compatibility for dynamic images using ACF image field before 1.6.
if ( array_key_exists( 'ID', $field ) ) {
return $field['ID'];
}
return '';
}
if (
is_object( $field ) &&
isset( $attributes[ $metaFieldPropertyNameKey ] ) &&
isset( $field->{$attributes[ $metaFieldPropertyNameKey ]} )
) {
return $field->{$attributes[ $metaFieldPropertyNameKey ]};
} elseif ( is_object( $field ) ) {
return '';
}
return $field;
}
/**
* Load the author custom fields.
*
* @param mixed $value The field value.
* @param string $author_id The author id.
* @param array $attributes The block attributes.
*
* @return mixed|string The field value.
*/
public static function load_author_acf_fields( $value, $author_id, $attributes ) {
return self::load_acf_fields( $value, 'user_' . $author_id, $attributes );
}
/**
* Load the link custom field.
*
* @param mixed $value The field value.
* @param string $id The object id.
* @param array $attributes The block attributes.
*
* @return mixed|string The field value.
*/
public static function load_link_acf_fields( $value, $id, $attributes ) {
return self::load_acf_fields(
$value,
$id,
$attributes,
'linkMetaFieldName',
'linkMetaFieldPropertyName'
);
}
/**
* Load the author link custom field.
*
* @param mixed $value The field value.
* @param string $author_id The author id.
* @param array $attributes The block attributes.
*
* @return mixed|string The field value.
*/
public static function load_author_link_acf_fields( $value, $author_id, $attributes ) {
return self::load_acf_fields(
$value,
'user_' . $author_id,
$attributes,
'linkMetaFieldName',
'linkMetaFieldPropertyName'
);
}
}
GenerateBlocks_Pro_Advanced_Custom_Fields::get_instance();
@@ -0,0 +1,361 @@
<?php
/**
* The class to integrate advance custom fields to dynamic content post meta.
*
* @package Generateblocks/Extend/DynamicTags
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* GenerateBlocks Pro Advanced custom fields integration
*
* @since 1.4.0
*/
class GenerateBlocks_Pro_Dynamic_Tags_ACF extends GenerateBlocks_Pro_Singleton {
/**
* Stored ACF option keys.
*
* @var array
*/
public $acf_option_fields = [];
/**
* Init function
* The post meta value.
*/
public function init() {
if ( ! class_exists( 'ACF' ) ) {
return; // Exit if Advanced Custom Fields plugin is not active.
}
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
add_filter(
'generateblocks_get_meta_pre_value',
[ $this, 'get_meta_pre_value' ],
10,
5
);
add_filter(
'generateblocks_dynamic_tags_post_record_response',
[ $this, 'add_acf_meta_to_post_record' ],
10,
3
);
add_filter(
'generateblocks_dynamic_tags_user_record_response',
[ $this, 'add_acf_meta_to_user_record' ],
10,
3
);
}
/**
* Add appropriate meta pre values.
*
* @param string|null $pre_value The pre - filtered value, or null if unset.
* @param int $id The entity ID used to fetch the meta value.
* @param string $key The meta key to fetch.
* @param string $callable function name to call. Should be a native WordPress function ( ex: get_post_meta).
*/
public function get_meta_pre_value( $pre_value, $id, $key, $callable ) {
switch ( $callable ) {
case 'get_post_meta':
$value = self::get_post_meta_pre_value( $pre_value, $id, $key );
break;
case 'get_user_meta':
$value = self::get_user_meta_pre_value( $pre_value, $id, $key );
break;
case 'get_term_meta':
$value = self::get_term_meta_pre_value( $pre_value, $id, $key );
break;
case 'get_option':
$value = self::get_option_pre_value( $pre_value, $id, $key );
break;
default:
}
return $value;
}
/**
* Register REST routes.
*
* @return void
*/
public function register_rest_routes() {
register_rest_route(
'generateblocks-pro/v1',
'/get-acf-option-fields',
[
'methods' => 'GET',
'callback' => [ $this, 'get_acf_option_fields_rest' ],
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
]
);
}
/**
*
* This function checks if the provided key is an Advanced Custom Fields (ACF) field key.
* If it is, the function retrieves the ACF field value using the `get_field` function. Sub fields
* can be passed to the $key param using a "." to separate the parent and sub fields (ex: parent_child.sub_field).
*
* @since 1.8.0
*
* @param string $value The current post meta value.
* @param int $id The ID of the post.
* @param string $key The post meta key.
* @param string $type The post meta type.
*
* @return mixed The updated post meta value. If the key is not an ACF field key, the original value is returned.
*/
public function maybe_get_field( $value, $id, $key, $type = '' ) {
// Bail if the ACF functions are undefined.
if ( ! function_exists( 'get_field' ) || ! function_exists( 'acf_get_meta' ) ) {
return $value;
}
$acf_id = $type ? "{$type}_{$id}" : $id;
$key_parts = array_map( 'trim', explode( '.', $key ) );
$parent_name = $key_parts[0];
$is_acf_field = false;
$parent_is_option = 'option' === $type ? $this->is_acf_option( $parent_name ) : false;
$acf_keys = [];
if ( $parent_is_option ) {
$acf_id = 'option';
$is_acf_field = true;
} else {
$acf_keys = acf_get_meta( $acf_id );
$is_acf_field = isset( $acf_keys[ $parent_name ] );
}
/**
* Filter the ACF field check to allow for custom logic.
*
* @param bool $is_acf_field Whether the provided key is an ACF field key.
* @param string|int $acf_id The ACF ID for the post. All options have the value of 'option'.
* @param array $acf_keys The ACF keys for the $acf_id.
* @param array $args Additional arguments: $value (the current post meta value), $id (the ID of the post), $key (the post meta key), and $type (the post meta type).
*
* @return bool Whether the provided key is an ACF field key.
*/
$is_acf_field = apply_filters(
'generateblocks_pro_dynamic_tags_is_acf_field',
$is_acf_field,
$acf_id,
$acf_keys,
[
$value,
$id,
$key,
$type,
]
);
if ( ! $is_acf_field ) {
return $value;
}
return get_field( $parent_name, $acf_id );
}
/**
* Filters the post meta value before it's returned.
*
* This function checks if the provided key is an Advanced Custom Fields( ACF ) field key.
* if it is, the function retrieves the ACF field value using the `get_field` function.
*
* @since 1.8.0
*
* @param string $value The current post meta value.
* @param int $post_id The ID of the post.
* @param string $key The post meta key.
*
* @return mixed The updated post meta value. if the key is not an ACF field key, the original value is returned.
*/
public function get_post_meta_pre_value( $value, $post_id, $key ) {
return $this->maybe_get_field( $value, $post_id, $key );
}
/**
* Filters the term meta value before it's returned.
*
* This function checks if the provided key is an Advanced Custom Fields (ACF) field key.
* If it is, the function retrieves the ACF field value using the `get_field` function.
*
* @param string $value The current term meta value.
* @param int $id The ID of the term to query meta from.
* @param string $key The term meta key.
*
* @return mixed The updated post meta value. If the key is not an ACF field key, the original value is returned.
*/
public function get_term_meta_pre_value( $value, $id, $key ) {
return $this->maybe_get_field( $value, $id, $key, 'term' );
}
/**
* Filters the user meta value before it's returned.
*
* This function checks if the provided key is an Advanced Custom Fields (ACF) field key.
* If it is, the function retrieves the ACF field value using the `get_field` function.
*
* @param string $value The current user meta value.
* @param int $id The ID of the user to query meta from.
* @param string $key The user meta key.
*
* @return mixed The updated post meta value. If the key is not an ACF field key, the original value is returned.
*/
public function get_user_meta_pre_value( $value, $id, $key ) {
return $this->maybe_get_field( $value, $id, $key, 'user' );
}
/**
* Filters the option value before it's returned.
*
* This function checks if the provided key is an Advanced Custom Fields (ACF) field key.
* If it is, the function retrieves the ACF field value using the `get_field` function.
*
* @param string $value The current option meta value.
* @param int $id The ACF ID of the option to retrieve.
* @param string $key The option key.
*
* @return mixed The updated post meta value. If the key is not an ACF field key, the original value is returned.
*/
public function get_option_pre_value( $value, $id, $key ) {
return $this->maybe_get_field( $value, $id, $key, 'option' );
}
/**
* Filters the post record to include ACF meta field keys and values.
*
* @param object $response Post object from the response.
* @param int $id ID of the post record.
*
* @return mixed
*/
public function add_acf_meta_to_post_record( $response, $id ) {
// Stop here if the ACF functions are undefined.
if ( ! function_exists( 'acf_get_meta' ) ) {
return $response;
}
$acf_meta = acf_get_meta( $id );
if ( $acf_meta ) {
$response->acf = array_filter(
$acf_meta,
function ( $key ) {
return strpos( $key, '_' ) !== 0;
},
ARRAY_FILTER_USE_KEY
);
}
return $response;
}
/**
* Filters the user record to include ACF meta field keys and values.
*
* @param object $response Post object from the response.
* @param int $id ID of the post record.
*
* @return mixed
*/
public function add_acf_meta_to_user_record( $response, $id ) {
// Stop here if the ACF functions are undefined.
if ( ! function_exists( 'acf_get_meta' ) ) {
return $response;
}
$acf_meta = acf_get_meta( "user_$id" );
if ( $acf_meta ) {
$response->acf = array_filter(
$acf_meta,
function ( $key ) {
return strpos( $key, '_' ) !== 0;
},
ARRAY_FILTER_USE_KEY
);
}
return $response;
}
/**
* Retrieves all ACf option fields using the default option prefix.
*
* @return array
*/
public function get_acf_option_fields() {
if ( ! function_exists( 'acf_get_option_meta' ) ) {
return [];
}
$options = array_filter(
acf_get_option_meta( 'options' ),
function( $key ) {
return strpos( $key, '_' ) !== 0;
},
ARRAY_FILTER_USE_KEY
);
$response = [];
foreach ( $options as $key => $array_value ) {
$response[ $key ] = $array_value[0];
}
return $response;
}
/**
* Rest controller for get-acf-option-fields.
*
* @return WP_REST_Response|WP_Error
*/
public function get_acf_option_fields_rest() {
if ( ! $this->acf_option_fields ) {
$this->acf_option_fields = $this->get_acf_option_fields();
}
return rest_ensure_response( $this->acf_option_fields );
}
/**
* Check if a given field name is an ACF option or not.
*
* @param string $field_name The name of the field to check.
* @return bool Return true if the option is an ACF option.
*/
public function is_acf_option( $field_name ) {
if ( ! $this->acf_option_fields ) {
$this->acf_option_fields = $this->get_acf_option_fields();
}
return isset( $this->acf_option_fields[ $field_name ] );
}
}
GenerateBlocks_Pro_Dynamic_Tags_ACF::get_instance()->init();
@@ -0,0 +1,62 @@
<?php
/**
* The class to integrate adjacent post dynamic tags.
*
* @package Generateblocks/Extend/DynamicTags
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* GenerateBlocks Pro adjacted post dynamic tags.
*
* @since 1.4.0
*/
class GenerateBlocks_Pro_Dynamic_Tags_Adjacent_Posts extends GenerateBlocks_Pro_Singleton {
/**
* Init.
*/
public function init() {
add_filter( 'generateblocks_dynamic_tag_id', array( $this, 'set_adjacent_post_ids' ), 10, 2 );
}
/**
* Set adjacent post ids.
*
* @param int $id The post id.
* @param array $options The options.
*/
public function set_adjacent_post_ids( $id, $options ) {
$source = $options['source'] ?? '';
if ( 'next-post' === $source ) {
$in_same_term = $options['inSameTerm'] ?? false;
$term_taxonomy = $options['sameTermTaxonomy'] ?? 'category';
$next_post = get_next_post( $in_same_term, '' );
if ( ! is_object( $next_post ) ) {
return false;
}
return $next_post->ID;
}
if ( 'previous-post' === $source ) {
$in_same_term = $options['inSameTerm'] ?? false;
$term_taxonomy = $options['sameTermTaxonomy'] ?? 'category';
$previous_post = get_previous_post( $in_same_term, '', $term_taxonomy );
if ( ! is_object( $previous_post ) ) {
return false;
}
return $previous_post->ID;
}
return $id;
}
}
GenerateBlocks_Pro_Dynamic_Tags_Adjacent_Posts::get_instance()->init();
@@ -0,0 +1,502 @@
<?php
/**
* The Dynamic Tags class file.
*
* @package GenerateBlocks_Pro\Dynamic_Tags
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for handling dynamic tags.
*
* @since 2.0.0
*/
class GenerateBlocks_Pro_Dynamic_Tags_Register extends GenerateBlocks_Pro_Singleton {
/**
* Initialize all hooks.
*
* @return void
*/
public function init() {
if ( ! class_exists( 'GenerateBlocks_Register_Dynamic_Tag' ) ) {
return;
}
add_action( 'init', [ $this, 'register' ] );
}
/**
* Register the tags.
*
* @return void
*/
public function register() {
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Archive Title', 'generateblocks-pro' ),
'tag' => 'archive_title',
'type' => 'archive',
'supports' => [],
'description' => __( 'Get the title for the current archive being viewed.', 'generateblocks-pro' ),
'return' => [ $this, 'get_archive_title' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Archive Description', 'generateblocks-pro' ),
'tag' => 'archive_description',
'type' => 'archive',
'supports' => [],
'description' => __( 'Get the description for the current archive being viewed.', 'generateblocks-pro' ),
'return' => [ $this, 'get_archive_description' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Option', 'generateblocks-pro' ),
'tag' => 'option',
'type' => 'option',
'supports' => [ 'meta' ],
'return' => [ $this, 'get_option' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Term Meta', 'generateblocks-pro' ),
'tag' => 'term_meta',
'type' => 'term',
'supports' => [ 'meta', 'source' ],
'description' => __( 'Access term meta by key for the specified term. Return value must be a string.', 'generateblocks-pro' ),
'return' => [ $this, 'get_term_meta' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'User Meta', 'generateblocks-pro' ),
'tag' => 'user_meta',
'type' => 'user',
'supports' => [ 'meta', 'source' ],
'description' => __( 'Access user meta by key for the specified user. Return value must be a string.', 'generateblocks-pro' ),
'return' => [ $this, 'get_user_meta' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Current year', 'generateblocks-pro' ),
'tag' => 'current_year',
'type' => 'site',
'supports' => [],
'return' => [ $this, 'get_current_year' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Site Title', 'generateblocks-pro' ),
'tag' => 'site_title',
'type' => 'site',
'supports' => [],
'return' => [ $this, 'get_site_title' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Site Tagline', 'generateblocks-pro' ),
'tag' => 'site_tagline',
'type' => 'site',
'supports' => [],
'return' => [ $this, 'get_site_tagline' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Site Logo URL', 'generateblocks-pro' ),
'tag' => 'site_logo_url',
'type' => 'site',
'supports' => [],
'return' => [ $this, 'get_site_logo_url' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Site URL', 'generateblocks-pro' ),
'tag' => 'site_url',
'type' => 'site',
'supports' => [],
'return' => [ $this, 'get_site_url' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Loop Index', 'generateblocks-pro' ),
'tag' => 'loop_index',
'type' => 'looper',
'supports' => [],
'visibility' => [
'context' => [
'generateblocks/loopIndex',
],
],
'options' => [
'zeroBased' => [
'type' => 'checkbox',
'label' => __( 'Use zero-based index', 'generateblocks-pro' ),
'help' => __( 'Enable this to start the loop index count from 0.', 'generateblocks-pro' ),
],
],
'description' => __( 'The numbered index of the loop item.', 'generateblocks-pro' ),
'return' => [ $this, 'get_loop_index' ],
]
);
new GenerateBlocks_Register_Dynamic_Tag(
[
'title' => __( 'Loop Item', 'generateblocks-pro' ),
'tag' => 'loop_item',
'type' => 'looper',
'supports' => [ 'properties' ],
'visibility' => [
'context' => [
'generateblocks/loopItem',
],
],
'description' => __( 'The current loop item data.', 'generateblocks-pro' ),
'return' => [ $this, 'get_loop_item' ],
]
);
}
/**
* Get the archive title.
*
* @param array $options The options.
* @param object $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_archive_title( $options, $block, $instance ) {
$output = '';
$id = $options['id'] ?? 0;
if ( is_category() ) {
$output = single_cat_title( '', false );
} elseif ( is_tag() ) {
$output = single_tag_title( '', false );
} elseif ( is_author() ) {
$output = get_the_author();
} elseif ( is_post_type_archive() ) {
$output = post_type_archive_title( '', false );
} elseif ( is_tax() ) {
$output = single_term_title( '', false );
} elseif ( is_home() ) {
$page = get_option( 'page_for_posts' ) ?? 0;
if ( $page ) {
$output = get_the_title( $page );
} else {
$output = __( 'Blog', 'generateblocks-pro' );
}
} elseif ( is_search() ) {
$output = get_search_query();
} elseif ( $id ) {
if ( term_exists( (int) $id ) ) {
$term = get_term( $id );
$output = $term->name;
} elseif ( is_string( $id ) ) {
// Assume it's a post type archive title.
$post_type_obj = get_post_type_object( $id );
$title = $post_type_obj->labels->name ?? '';
if ( $title ) {
/**
* Core Filter. Filters the post type archive title.
*
* @param string $post_type_name Post type 'name' label.
* @param string $post_type Post type.
*/
$output = apply_filters( 'post_type_archive_title', $title, $id );
}
}
}
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the archive description.
*
* @param array $options The options.
* @param object $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_archive_description( $options, $block, $instance ) {
$output = get_the_archive_description();
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the option.
*
* @param array $options The options.
* @return string
*/
public static function get_option( $options ) {
$default = $options['default'] ?? '';
$key = $options['key'] ?? '';
$key_parts = array_map( 'trim', explode( '.', $key ) );
$parent_name = $key_parts[0];
$output = '';
if ( empty( $key ) ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options );
}
$allowed_options = [
'siteurl',
'blogname',
'blogdescription',
'home',
'time_format',
'user_count',
];
$acf_keys = array_keys(
GenerateBlocks_Pro_Dynamic_Tags_ACF::get_instance()->get_acf_option_fields()
);
/**
* $allowed_options contains an array of keys from the options table along with the
* acf option keys. Disallowed keys will return an empty string.
*
* @since 2.0.0
* @param array $allowed_options Array of allowed option keys.
*/
$allowed_options = apply_filters(
'generateblocks_dynamic_tags_allowed_options',
array_merge( $allowed_options, $acf_keys )
);
if ( ! in_array( $parent_name, $allowed_options, true ) ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options );
}
$value = GenerateBlocks_Meta_Handler::get_option( $key, true, $default );
if ( ! $value ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options );
}
add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
$output = wp_kses_post( $value );
remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options );
}
/**
* Get the term meta.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_term_meta( $options, $block, $instance ) {
$id = GenerateBlocks_Dynamic_Tags::get_id( $options, 'term', $instance );
if ( ! $id ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( '', $options, $instance );
}
$key = $options['key'] ?? '';
$output = '';
if ( empty( $key ) ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
$value = GenerateBlocks_Meta_Handler::get_term_meta( $id, $key, true );
if ( ! $value ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
$output = wp_kses_post( $value );
remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the user meta.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_user_meta( $options, $block, $instance ) {
$id = GenerateBlocks_Dynamic_Tags::get_id( $options, 'user', $instance );
if ( ! $id ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( '', $options, $instance );
}
$key = $options['key'] ?? '';
$output = '';
if ( empty( $key ) ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
$value = GenerateBlocks_Meta_Handler::get_user_meta( $id, $key, true );
if ( ! $value ) {
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
$output = wp_kses_post( $value );
remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Tags', 'expand_allowed_html' ], 10, 2 );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the current year.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
*
* @return string
*/
public static function get_current_year( $options, $block, $instance ) {
$output = wp_date( 'Y' );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the site title from settings.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_site_title( $options, $block, $instance ) {
$output = get_option( 'blogname' );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the site tagline from settings.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_site_tagline( $options, $block, $instance ) {
$output = get_option( 'blogdescription' );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the site logo URL.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_site_logo_url( $options, $block, $instance ) {
$custom_logo_id = get_theme_mod( 'custom_logo' );
$output = '';
if ( $custom_logo_id ) {
$url = wp_get_attachment_url( $custom_logo_id );
if ( $url ) {
$output = esc_url( $url );
}
}
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the site URL.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string
*/
public static function get_site_url( $options, $block, $instance ) {
$output = esc_url( site_url() );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
/**
* Get the index of the current looper block loop.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return int The loop index number.
*/
public static function get_loop_index( $options, $block, $instance ) {
$use_zero_based = $options['zeroBased'] ?? false;
$loop_index = (int) isset( $instance->context['generateblocks/loopIndex'] )
? $instance->context['generateblocks/loopIndex']
: -1;
if ( $use_zero_based ) {
--$loop_index;
}
if ( $loop_index > -1 ) {
return (string) $loop_index;
}
}
/**
* Get the current loop item.
*
* @param array $options The options.
* @param array $block The block.
* @param object $instance The block instance.
* @return string Value of the loop item or a given key's value from the loop item.
*/
public static function get_loop_item( $options, $block, $instance ) {
$key = $options['key'] ?? '';
$fallback = $options['fallback'] ?? '';
$loop_item = $instance->context['generateblocks/loopItem'] ?? [];
$output = GenerateBlocks_Meta_Handler::get_value( $key, $loop_item, true, $fallback );
return GenerateBlocks_Dynamic_Tag_Callbacks::output( $output, $options, $instance );
}
}
GenerateBlocks_Pro_Dynamic_Tags_Register::get_instance()->init();
@@ -0,0 +1,455 @@
<?php
/**
* This file handles the Accordion functions.
*
* @package GenerateBlocksPro/Extend/Interactions/Accordion
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Accordion functions.
*
* @since 1.5.0
*/
class GenerateBlocks_Pro_Block_Variant_Accordion extends GenerateBlocks_Pro_Singleton {
/**
* Constructor.
*/
public function __construct() {
parent::__construct();
if ( ! version_compare( GENERATEBLOCKS_VERSION, '1.7.0-alpha.1', '>=' ) ) {
return;
}
add_filter( 'generateblocks_defaults', [ $this, 'set_defaults' ] );
add_filter( 'generateblocks_after_container_open', [ $this, 'enqueue_scripts' ], 10, 2 );
add_filter( 'generateblocks_attr_container', [ $this, 'set_container_attributes' ], 10, 4 );
add_filter( 'generateblocks_attr_dynamic-button', [ $this, 'set_button_attributes' ], 10, 2 );
add_action( 'generateblocks_block_one_time_css_data', [ $this, 'generate_css' ], 10, 3 );
add_action( 'generateblocks_block_css_data', [ $this, 'do_dynamic_css' ], 10, 3 );
add_filter( 'generateblocks_before_container_open', [ $this, 'open_accordion_content_container' ], 1, 3 );
add_filter( 'generateblocks_after_container_close', [ $this, 'close_accordion_content_container' ], 100, 3 );
add_filter( 'generateblocks_onboarding_user_meta_properties', [ $this, 'define_add_accordion_item_onboarding_property' ], 10, 1 );
add_filter( 'register_block_type_args', [ $this, 'block_type_args' ], 10, 2 );
add_action( 'wp_footer', [ $this, 'faq_schema_script' ] );
add_filter( 'render_block', [ $this, 'gather_schema_data' ], 10, 3 );
}
/**
* Set our attribute defaults.
*
* @param array $defaults Existing defaults.
*/
public function set_defaults( $defaults ) {
$defaults['container']['accordionItemOpen'] = false;
$defaults['container']['accordionMultipleOpen'] = false;
$defaults['container']['accordionTransition'] = '';
$defaults['button']['accordionItemOpen'] = false;
return $defaults;
}
/**
* Enqueue our accordion script.
*
* @param string $content Block content.
* @param array $attributes Block attributes.
*/
public function enqueue_scripts( $content, $attributes ) {
if ( ! empty( $attributes['variantRole'] ) && 'accordion' === $attributes['variantRole'] ) {
wp_enqueue_script(
'generateblocks-accordion',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/accordion.js',
array(),
GENERATEBLOCKS_PRO_VERSION,
true
);
}
return $content;
}
/**
* Set our Container block HTML attributes.
*
* @param array $attributes HTML attributes.
* @param array $settings Block settings.
* @param string $context Context of the filter.
* @param WP_Block $block Block instance.
*/
public function set_container_attributes( $attributes, $settings, $context, $block ) {
if ( isset( $settings['variantRole'] ) && 'accordion' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-accordion';
if ( $settings['accordionMultipleOpen'] ) {
$attributes['data-accordion-multiple-open'] = true;
}
}
if ( isset( $settings['variantRole'] ) && 'accordion-item' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-accordion__item';
if ( $settings['accordionItemOpen'] ) {
$attributes['class'] .= ' gb-accordion__item-open';
}
if ( $settings['accordionTransition'] ) {
$attributes['data-transition'] = $settings['accordionTransition'];
}
}
if ( isset( $settings['variantRole'] ) && 'accordion-content' === $settings['variantRole'] ) {
$transition = isset( $block->context['generateblocks-pro/accordionTransition'] )
? $block->context['generateblocks-pro/accordionTransition']
: '';
if ( 'slide' !== $transition ) {
$attributes['class'] .= ' gb-accordion__content';
}
// Unset the accordion content ID. This ID is added to the wrapping div
// in the open_accordion_content_container function.
$attributes['id'] = '';
}
if ( isset( $settings['variantRole'] ) && 'accordion-toggle' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-accordion__toggle';
if ( $settings['accordionItemOpen'] ) {
$attributes['class'] .= ' gb-block-is-current';
}
$attributes['role'] = 'button';
$attributes['tabindex'] = '0';
}
return $attributes;
}
/**
* Set our dynamic Button block HTML attributes.
*
* @param array $attributes HTML attributes.
* @param array $settings Block settings.
*/
public function set_button_attributes( $attributes, $settings ) {
if ( isset( $settings['variantRole'] ) && 'accordion-toggle' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-accordion__toggle';
if ( $settings['accordionItemOpen'] ) {
$attributes['class'] .= ' gb-block-is-current';
}
}
return $attributes;
}
/**
* Generate our one-time accordion CSS.
*
* @param string $name The block name.
* @param array $settings Block settings.
* @param object $css The CSS object.
*/
public function generate_css( $name, $settings, $css ) {
if ( 'button' === $name ) {
$css->set_selector( '.gb-accordion__item:not(.gb-accordion__item-open) > .gb-button .gb-accordion__icon-open' );
$css->add_property( 'display', 'none' );
$css->set_selector( '.gb-accordion__item.gb-accordion__item-open > .gb-button .gb-accordion__icon' );
$css->add_property( 'display', 'none' );
}
}
/**
* Generate our CSS for our options.
*
* @since 1.0
* @param string $name Name of the block.
* @param array $settings Our available settings.
* @param object $css Current desktop CSS object.
*/
public function do_dynamic_css( $name, $settings, $css ) {
if ( 'container' === $name && isset( $settings['variantRole'] ) ) {
$accordion_transition = ! empty( $settings['accordionTransition'] ) ? $settings['accordionTransition'] : '';
$selector = function_exists( 'generateblocks_get_css_selector' )
? generateblocks_get_css_selector( 'container', $settings )
: '.gb-container-' . $settings['uniqueId'];
if ( 'accordion-item' === $settings['variantRole'] ) {
if ( '' === $accordion_transition || 'fade' === $accordion_transition ) {
$css->set_selector( $selector . ':not(.gb-accordion__item-open) > .gb-accordion__content' );
$css->add_property( 'display', 'none' );
if ( 'fade' === $accordion_transition ) {
$css->set_selector( $selector . '.gb-accordion__item-open > .gb-accordion__content' );
$css->add_property( 'opacity', 1 );
$css->set_selector( $selector . '.gb-accordion__item-transition > .gb-accordion__content' );
$css->add_property( 'opacity', 0 );
}
}
if ( 'slide' === $accordion_transition ) {
$css->set_selector( $selector . ' > .gb-accordion__content' );
$css->add_property( 'will-change', 'max-height' );
$css->add_property( 'max-height', 0 );
$css->add_property( 'overflow', 'hidden' );
$css->add_property( 'visibility', 'hidden' );
$css->set_selector( $selector . '.gb-accordion__item-open > .gb-accordion__content' );
$css->add_property( 'max-height', 'inherit' );
$css->add_property( 'visibility', 'visible' );
}
}
if ( 'accordion-toggle' === $settings['variantRole'] ) {
$css->set_selector( $selector );
$css->add_property( 'cursor', 'pointer' );
}
}
}
/**
* Inject our opening accordion content div.
*
* @param string $content Block content.
* @param array $attributes Block attributes.
* @param WP_Block $block Block instance.
*/
public function open_accordion_content_container( $content, $attributes, $block ) {
$transition = isset( $block->context['generateblocks-pro/accordionTransition'] )
? $block->context['generateblocks-pro/accordionTransition']
: '';
if ( isset( $attributes['variantRole'] ) && 'accordion-content' === $attributes['variantRole'] && 'slide' === $transition ) {
if ( ! empty( $attributes['anchor'] ) ) {
$content = '<div id="' . esc_attr( $attributes['anchor'] ) . '" class="gb-accordion__content">' . $content;
} else {
$content = '<div class="gb-accordion__content">' . $content;
}
}
return $content;
}
/**
* Inject our closing accordion content div.
*
* @param string $content Block content.
* @param array $attributes Block attributes.
* @param WP_Block $block Block instance.
*/
public function close_accordion_content_container( $content, $attributes, $block ) {
$transition = isset( $block->context['generateblocks-pro/accordionTransition'] )
? $block->context['generateblocks-pro/accordionTransition']
: '';
if ( isset( $attributes['variantRole'] ) && 'accordion-content' === $attributes['variantRole'] && 'slide' === $transition ) {
$content .= '</div>';
}
return $content;
}
/**
* Define the onboarding key for adding accordion items.
*
* @param array $properties The registered keys.
*
* @return array
*/
public function define_add_accordion_item_onboarding_property( $properties ) {
$properties['add_accordion_item'] = array( 'type' => 'boolean' );
return $properties;
}
/**
* Filter block args.
*
* @param array $args Existing args.
* @param string $name The block name.
*/
public function block_type_args( $args, $name ) {
if ( 'generateblocks/container' === $name ) {
if ( ! isset( $args['provides_context'] ) || ! is_array( $args['provides_context'] ) ) {
$args['provides_context'] = [];
}
$args['provides_context'] = array_merge(
$args['provides_context'],
[
'generateblocks-pro/accordionTransition' => 'accordionTransition',
'generateblocks-pro/faqSchema' => 'faqSchema',
]
);
if ( ! isset( $args['uses_context'] ) || ! is_array( $args['uses_context'] ) ) {
$args['uses_context'] = [];
}
$args['uses_context'] = array_merge(
$args['uses_context'],
[
'generateblocks-pro/accordionTransition',
'generateblocks-pro/faqSchema',
]
);
}
if ( 'generateblocks/button' === $name ) {
if ( ! isset( $args['provides_context'] ) || ! is_array( $args['provides_context'] ) ) {
$args['provides_context'] = [];
}
$args['provides_context'] = array_merge(
$args['provides_context'],
[
'generateblocks-pro/faqSchema' => 'faqSchema',
]
);
if ( ! isset( $args['uses_context'] ) || ! is_array( $args['uses_context'] ) ) {
$args['uses_context'] = [];
}
$args['uses_context'] = array_merge(
$args['uses_context'],
[
'generateblocks-pro/faqSchema',
]
);
}
return $args;
}
/**
* Remove all HTML and line-breaks/whitespace from the content.
*
* @param string $content The block content.
*/
public static function strip_html( $content ) {
$allowed_tags = [ '<h1>', '<h2>', '<h3>', '<h4>', '<h5>', '<h6>', '<br>', '<ol>', '<ul>', '<li>', '<a>', '<p>', '<b>', '<strong>', '<i>', '<em>' ];
$allowed_tags = implode( '', $allowed_tags );
$block_text = strip_tags( $content, $allowed_tags );
$block_text = preg_replace( "/\n/", '', $block_text ); // Remove new lines.
return $block_text;
}
/**
* Gather FAQ schema from accordions.
*
* @since 1.6.0
* @param string $block_content The block content.
* @param array $block The block attributes/data.
* @param WP_Block $instance The block instance.
* @return array
*/
public function gather_schema_data( $block_content, $block, $instance ) {
if (
isset( $block['attrs']['variantRole'] ) &&
isset( $instance->context ) &&
! empty( $instance->context['generateblocks-pro/faqSchema'] )
) {
if ( 'accordion-toggle' === $block['attrs']['variantRole'] ) {
add_filter(
'generateblocks_faq_schema_data',
function( $data ) use ( $block_content ) {
$data[] = [ 'question' => self::strip_html( $block_content ) ];
return $data;
},
1
);
}
if ( 'accordion-content' === $block['attrs']['variantRole'] ) {
add_filter(
'generateblocks_faq_schema_data',
function( $data ) use ( $block_content ) {
foreach ( $data as $key => $info ) {
// If we have a question but no answer, assume this is the answer.
if ( ! empty( $info['question'] ) && empty( $info['answer'] ) ) {
preg_match( '/<img.+src=[\'"](?P<src>.+?)[\'"].*>/i', $block_content, $image );
if ( ! empty( $image['src'] ) ) {
$data[ $key ]['image'] = $image['src'];
}
$data[ $key ]['answer'] = self::strip_html( $block_content );
break;
}
}
return $data;
},
2
);
}
}
return $block_content;
}
/**
* Output the accordion FAQ schema to the footer.
*
* @since 1.6.0
*/
public function faq_schema_script() {
$faq_schema_data = apply_filters(
'generateblocks_faq_schema_data',
[]
);
if ( empty( $faq_schema_data ) ) {
return;
}
// Remove any duplicated questions/answers.
// @see https://stackoverflow.com/a/308955.
$serialized = array_map( 'json_encode', $faq_schema_data );
$unique = array_unique( $serialized );
$faq_schema_data = array_values( array_intersect_key( $faq_schema_data, $unique ) );
$faq_schema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'name' => get_the_title(),
'mainEntity' => [],
];
foreach ( $faq_schema_data as $data ) {
$question = [
'@type' => 'Question',
'name' => $data['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $data['answer'],
],
];
if ( ! empty( $data['image'] ) ) {
$question['acceptedAnswer']['image'] = [
'@type' => 'ImageObject',
'contentUrl' => esc_url_raw( $data['image'] ),
];
}
$faq_schema['mainEntity'][] = $question;
}
if ( count( $faq_schema['mainEntity'] ) > 0 ) {
printf( '<script type="application/ld+json">%s</script>', wp_json_encode( $faq_schema ) );
}
}
}
GenerateBlocks_Pro_Block_Variant_Accordion::get_instance();
@@ -0,0 +1,196 @@
<?php
/**
* Our Tabs block.
*
* @package GenerateBlocks/Extend/Interactions/Tabs
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* GenerateBlocks Pro Tabs.
*
* @since 1.4.0
*/
class GenerateBlocks_Pro_Tabs_Variation extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
protected function __construct() {
parent::__construct();
if ( ! version_compare( GENERATEBLOCKS_VERSION, '1.7.0-alpha.1', '>=' ) ) {
return;
}
add_filter( 'generateblocks_defaults', [ $this, 'set_defaults' ] );
add_action( 'generateblocks_block_one_time_css_data', [ $this, 'generate_css' ], 10, 3 );
add_action( 'generateblocks_block_css_data', [ $this, 'do_dynamic_css' ], 10, 3 );
add_filter( 'generateblocks_attr_container', [ $this, 'set_container_attributes' ], 10, 2 );
add_filter( 'generateblocks_attr_dynamic-button', [ $this, 'set_button_attributes' ], 10, 2 );
add_filter( 'generateblocks_after_container_open', [ $this, 'enqueue_scripts' ], 10, 2 );
add_filter( 'generateblocks_onboarding_user_meta_properties', [ $this, 'define_add_tab_item_onboarding_property' ], 10, 1 );
}
/**
* Set our attribute defaults.
*
* @param array $defaults Existing defaults.
*/
public function set_defaults( $defaults ) {
$defaults['container']['defaultOpenedTab'] = '';
$defaults['container']['tabItemOpen'] = false;
$defaults['container']['tabTransition'] = '';
$defaults['container']['borderColorCurrent'] = false;
$defaults['container']['backgroundColorCurrent'] = '';
$defaults['container']['textColorCurrent'] = '';
$defaults['button']['tabItemOpen'] = false;
return $defaults;
}
/**
* Enqueue our tabs script.
*
* @param string $content Block content.
* @param array $attributes Block attributes.
*/
public function enqueue_scripts( $content, $attributes ) {
if ( ! empty( $attributes['variantRole'] ) && 'tabs' === $attributes['variantRole'] ) {
wp_enqueue_script(
'generateblocks-tabs',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/tabs.js',
array(),
GENERATEBLOCKS_PRO_VERSION,
true
);
}
return $content;
}
/**
* Generate our one-time tabs CSS.
*
* @param string $name The block name.
* @param array $settings Block settings.
* @param object $css The CSS object.
*/
public function generate_css( $name, $settings, $css ) {
if ( 'container' === $name ) {
$css->set_selector( '.gb-container.gb-tabs__item:not(.gb-tabs__item-open)' );
$css->add_property( 'display', 'none' );
}
}
/**
* Generate our CSS for our options.
*
* @since 1.0
* @param string $name Name of the block.
* @param array $settings Our available settings.
* @param object $css Current desktop CSS object.
*/
public function do_dynamic_css( $name, $settings, $css ) {
if ( 'container' === $name && isset( $settings['variantRole'] ) ) {
$selector = function_exists( 'generateblocks_get_css_selector' )
? generateblocks_get_css_selector( 'container', $settings )
: '.gb-container-' . $settings['uniqueId'];
if ( 'tab-button' === $settings['variantRole'] ) {
$css->set_selector( $selector . ':not(.gb-block-is-current)' );
$css->add_property( 'cursor', 'pointer' );
}
if ( 'tab-items' === $settings['variantRole'] && 'fade' === $settings['tabTransition'] ) {
$css->set_selector( $selector . ' > .gb-tabs__item-open' );
$css->add_property( 'opacity', 1 );
$css->set_selector( $selector . ' > .gb-tabs__item-transition' );
$css->add_property( 'opacity', 0 );
}
}
}
/**
* Set our Container block HTML attributes.
*
* @param array $attributes HTML attributes.
* @param array $settings Block settings.
*/
public function set_container_attributes( $attributes, $settings ) {
if ( isset( $settings['variantRole'] ) ) {
if ( 'tabs' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs';
$attributes['data-opened-tab'] = $settings['defaultOpenedTab'];
}
if ( 'tab-items' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs__items';
}
if ( 'tab-buttons' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs__buttons';
}
if ( 'tab-button' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs__button';
if ( true === $settings['tabItemOpen'] ) {
$attributes['class'] .= ' gb-block-is-current';
}
$attributes['role'] = 'button';
$attributes['tabindex'] = '0';
}
if ( 'tab-item' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs__item';
if ( $settings['tabItemOpen'] ) {
$attributes['class'] .= ' gb-tabs__item-open';
}
}
}
return $attributes;
}
/**
* Set our Button block HTML attributes.
*
* @param array $attributes HTML attributes.
* @param array $settings Block settings.
*/
public function set_button_attributes( $attributes, $settings ) {
if ( isset( $settings['variantRole'] ) ) {
if ( 'tab-button' === $settings['variantRole'] ) {
$attributes['class'] .= ' gb-tabs__button';
if ( $settings['tabItemOpen'] ) {
$attributes['class'] .= ' gb-block-is-current';
}
}
}
return $attributes;
}
/**
* Define the onboarding key for adding tab items.
*
* @param array $properties The registered keys.
*
* @return array
*/
public function define_add_tab_item_onboarding_property( $properties ) {
$properties['add_tab_item'] = array( 'type' => 'boolean' );
return $properties;
}
}
GenerateBlocks_Pro_Tabs_Variation::get_instance();
@@ -0,0 +1,186 @@
<?php
/**
* Extend the Looper block.
*
* @package GenerateBlocksPro\Extend\Looper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Extend the default Query block.
*
* @since 2.0.0
*/
class GenerateBlocks_Pro_Block_Looper extends GenerateBlocks_Pro_Singleton {
/**
* Init class.
*
* @return void
*/
public function init() {
add_filter( 'generateblocks_looper_render_loop_items', [ $this, 'render_loop_items' ], 10, 5 );
}
/**
* Render loop items based on the query type.
*
* @param string $output The block output.
* @param string $query_type The query type.
* @param array|object $query_data The query data.
* @param WP_Block $block The block instance.
*
* @return string The render content.
*/
public function render_loop_items( $output, $query_type, $query_data, $block ) {
if ( GenerateBlocks_Pro_Block_Query::TYPE_POST_META === $query_type ) {
return self::render_post_meta_loop_items( $query_data, $block );
}
if ( GenerateBlocks_Pro_Block_Query::TYPE_OPTION === $query_type ) {
return self::render_option_loop_items( $query_data, $block );
}
return $output;
}
/**
* Render the repeater items for the Looper block.
*
* @param array $items The items to loop over.
* @param WP_Block $block The block instance.
* @return string The rendered content.
*/
public static function render_post_meta_loop_items( $items, $block ) {
$query_id = $block->context['generateblocks/queryData']['id'] ?? null;
$page_key = $query_id ? 'query-' . $query_id . '-page' : 'query-page';
$args = $block->context['generateblocks/queryData']['args'] ?? [];
$per_page = $args['posts_per_page'] ?? get_option( 'posts_per_page' );
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // phpcs:ignore -- No data processing happening.
$page_index = $page - 1; // Zero based index for pages.
$offset = $page_index * $per_page;
$content = '';
$index = $offset + 1;
// Adjust value to support array_slice.
if ( '-1' === $per_page ) {
$per_page = count( $items );
}
if ( is_array( $items ) ) {
$items = array_slice( $items, $offset, $per_page );
foreach ( $items as $item ) {
// Get the current index of the Loop.
$content .= (
new WP_Block(
$block->parsed_block['innerBlocks'][0],
array(
'postType' => get_post_type(),
'postId' => get_the_ID(),
'generateblocks/queryType' => GenerateBlocks_Pro_Block_Query::TYPE_POST_META,
'generateblocks/loopIndex' => $index,
'generateblocks/loopItem' => $item,
)
)
)->render( array( 'dynamic' => false ) );
$index++;
}
return $content;
}
// Fallback to support previews in Elements.
$content = (
new WP_Block(
$block->parsed_block['innerBlocks'][0],
array(
'postType' => 'post',
'postId' => 0,
'generateblocks/queryType' => GenerateBlocks_Pro_Block_Query::TYPE_POST_META,
'generateblocks/loopIndex' => 1,
'generateblocks/loopItem' => [ 'ID' => 0 ],
)
)
)->render( array( 'dynamic' => false ) );
return $content;
}
/**
* Render the repeater items for the Looper block.
*
* @param array $items The items to loop over.
* @param WP_Block $block The block instance.
* @return string The rendered content.
*/
public static function render_option_loop_items( $items, $block ) {
$query_id = $block->context['generateblocks/queryData']['id'] ?? null;
$page_key = $query_id ? 'query-' . $query_id . '-page' : 'query-page';
$args = $block->context['generateblocks/queryData']['args'] ?? [];
$per_page = $args['posts_per_page'] ?? get_option( 'posts_per_page' );
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // phpcs:ignore -- No data processing happening.
$page_index = $page - 1; // Zero based index for pages.
$offset = $page_index * $per_page;
$content = '';
$index = $offset + 1;
// Adjust value to support array_slice.
if ( '-1' === $per_page ) {
$per_page = count( $items );
}
$inner_blocks = $block->parsed_block['innerBlocks'];
if ( ! $inner_blocks ) {
return $content;
}
if ( $items ) {
$items = array_slice( $items, $offset, $per_page );
foreach ( $items as $item ) {
// Get the current index of the Loop.
$content .= (
new WP_Block(
$inner_blocks[0],
array(
'postType' => $item['post_type'] ?? null,
'postId' => $item['ID'] ?? $item['id'] ?? 0,
'generateblocks/queryType' => GenerateBlocks_Pro_Block_Query::TYPE_OPTION,
'generateblocks/loopIndex' => $index,
'generateblocks/loopItem' => $item,
)
)
)->render( array( 'dynamic' => false ) );
$index++;
}
return $content;
}
// Fallback to support previews in Elements.
$content = (
new WP_Block(
$inner_blocks[0],
array(
'postType' => 'post',
'postId' => 0,
'generateblocks/queryType' => GenerateBlocks_Pro_Block_Query::TYPE_OPTION,
'generateblocks/loopIndex' => 1,
'generateblocks/loopItem' => [ 'ID' => 0 ],
)
)
)->render( array( 'dynamic' => false ) );
return $content;
}
}
GenerateBlocks_Pro_Block_Looper::get_instance()->init();
@@ -0,0 +1,489 @@
<?php
/**
* Menu Item Conditions functionality.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class GenerateBlocks_Pro_Menu_Item_Conditions
*/
class GenerateBlocks_Pro_Menu_Item_Conditions extends GenerateBlocks_Pro_Singleton {
/**
* Cache for evaluated conditions to avoid repeated checks.
*
* @var array
*/
private $condition_cache = [];
/**
* Initialize the class.
*/
public function init() {
// Don't initialize if block conditions are disabled.
if ( ! generateblocks_pro_block_conditions_enabled() ) {
return;
}
// Register post meta for menu items.
add_action( 'init', [ $this, 'register_post_meta' ] );
// Add fields to menu item editor.
add_action( 'wp_nav_menu_item_custom_fields', [ $this, 'add_condition_fields' ], 20, 5 );
// Save menu item conditions.
add_action( 'wp_update_nav_menu_item', [ $this, 'save_menu_item_conditions' ], 10, 2 );
// Filter menu items based on conditions.
add_filter( 'wp_nav_menu_objects', [ $this, 'filter_menu_items_by_conditions' ], 10, 2 );
// Add to condition usage tracking.
add_filter( 'generateblocks_condition_usage_handlers', [ $this, 'add_menu_usage_handler' ], 10, 2 );
// Add admin scripts for dynamic UI.
add_action( 'admin_enqueue_scripts', [ $this, 'add_admin_scripts' ] );
// AJAX handler for condition search.
add_action( 'wp_ajax_gb_search_conditions', [ $this, 'ajax_search_conditions' ] );
// AJAX handler for loading more conditions.
add_action( 'wp_ajax_gb_load_more_conditions', [ $this, 'ajax_load_more_conditions' ] );
}
/**
* Register the post meta.
*/
public function register_post_meta() {
register_post_meta(
'nav_menu_item',
'_gb_menu_condition',
[
'show_in_rest' => true,
'single' => true,
'type' => 'string',
]
);
register_post_meta(
'nav_menu_item',
'_gb_menu_condition_invert',
[
'show_in_rest' => true,
'single' => true,
'type' => 'boolean',
]
);
}
/**
* Get the conditions limit per page.
*/
private function get_conditions_limit() {
return apply_filters( 'generateblocks_menu_conditions_limit', 100 );
}
/**
* Add condition fields to menu items.
*
* @param string $item_id Menu item ID as a numeric string.
* @param WP_Post $menu_item Menu item data object.
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass|null $args An object of menu item arguments.
* @param int $current_object_id Nav menu ID.
*/
public function add_condition_fields( $item_id, $menu_item, $depth, $args, $current_object_id ) {
$condition_id = get_post_meta( $item_id, '_gb_menu_condition', true );
$invert = get_post_meta( $item_id, '_gb_menu_condition_invert', true );
// Get conditions for dropdown.
static $conditions_cache = null;
if ( null === $conditions_cache ) {
// Get the most recent conditions.
$conditions_cache = get_posts(
[
'post_type' => 'gblocks_condition',
'posts_per_page' => $this->get_conditions_limit(),
'orderby' => 'date',
'order' => 'DESC',
'post_status' => 'publish',
'suppress_filters' => false,
'no_found_rows' => true,
]
);
}
$conditions = $conditions_cache;
// Always include the currently selected condition if it's not in the list.
if ( $condition_id && 'gblocks_condition' === get_post_type( $condition_id ) ) {
$found = false;
foreach ( $conditions as $condition ) {
if ( $condition->ID === $condition_id ) {
$found = true;
break;
}
}
if ( ! $found ) {
$selected_condition = get_post( $condition_id );
if ( $selected_condition ) {
// Add it to the list for this menu item only.
$conditions = array_merge( [ $selected_condition ], $conditions );
}
}
}
// Check if there are more conditions than we're showing.
$total_conditions = wp_count_posts( 'gblocks_condition' )->publish;
$has_more = $total_conditions > count( $conditions_cache );
?>
<p class="field-gb-menu-condition description description-wide">
<label for="gb-menu-condition-<?php echo esc_attr( $item_id ); ?>">
<?php esc_html_e( 'Display Condition', 'generateblocks-pro' ); ?>
</label>
<div class="gb-menu-condition-wrapper" style="display: flex; align-items: center; gap: 10px;">
<select
name="gb-menu-condition[<?php echo esc_attr( $item_id ); ?>]"
id="gb-menu-condition-<?php echo esc_attr( $item_id ); ?>"
class="gb-menu-condition-select"
style="flex: 1;"
data-item-id="<?php echo esc_attr( $item_id ); ?>"
data-page="1"
>
<option value=""><?php esc_html_e( 'No condition', 'generateblocks-pro' ); ?></option>
<?php foreach ( $conditions as $condition ) : ?>
<option value="<?php echo esc_attr( $condition->ID ); ?>" <?php selected( $condition_id, $condition->ID ); ?>>
<?php echo esc_html( $condition->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
<?php if ( $has_more ) : ?>
<button
type="button"
class="button gb-load-more-conditions"
data-item-id="<?php echo esc_attr( $item_id ); ?>"
>
<?php esc_html_e( 'Load More', 'generateblocks-pro' ); ?>
</button>
<?php endif; ?>
</div>
<span class="description" style="display: block; margin-top: 5px;">
<?php esc_html_e( 'Choose a condition to control when this menu item appears.', 'generateblocks-pro' ); ?>
</span>
</p>
<p class="field-gb-menu-condition-invert description description-wide" style="<?php echo $condition_id ? '' : 'display: none;'; ?>">
<label>
<input
type="checkbox"
name="gb-menu-condition-invert[<?php echo esc_attr( $item_id ); ?>]"
value="1"
<?php checked( $invert, '1' ); ?>
/>
<?php esc_html_e( 'Invert condition', 'generateblocks-pro' ); ?>
</label>
<span class="description" style="display: block"><?php esc_html_e( 'Hide the menu item when the condition is true instead of false.', 'generateblocks-pro' ); ?></span>
</p>
<?php wp_nonce_field( 'update-menu-item-condition', 'menu-item-condition-nonce' ); ?>
<?php
}
/**
* Save the menu item conditions.
*
* @param int $menu_id The menu ID.
* @param int $menu_item_id The menu item ID.
* @return mixed
*/
public function save_menu_item_conditions( $menu_id, $menu_item_id ) {
// Verify nonce.
if (
! isset( $_POST['menu-item-condition-nonce'] )
|| ! wp_verify_nonce( $_POST['menu-item-condition-nonce'], 'update-menu-item-condition' )
) {
return $menu_id;
}
// Save condition from dropdown.
$condition_id = isset( $_POST['gb-menu-condition'][ $menu_item_id ] )
? sanitize_text_field( $_POST['gb-menu-condition'][ $menu_item_id ] )
: '';
if ( $condition_id ) {
update_post_meta( $menu_item_id, '_gb_menu_condition', $condition_id );
} else {
delete_post_meta( $menu_item_id, '_gb_menu_condition' );
}
// Save invert setting.
$invert = isset( $_POST['gb-menu-condition-invert'][ $menu_item_id ] ) ? '1' : '0';
if ( $condition_id && '1' === $invert ) {
update_post_meta( $menu_item_id, '_gb_menu_condition_invert', '1' );
} else {
delete_post_meta( $menu_item_id, '_gb_menu_condition_invert' );
}
return $menu_id;
}
/**
* Filter menu items based on conditions.
*
* @param array $sorted_menu_items The menu items, sorted by menu order.
* @param stdClass $args The menu arguments.
* @return array Filtered menu items.
*/
public function filter_menu_items_by_conditions( $sorted_menu_items, $args ) {
if ( empty( $sorted_menu_items ) ) {
return $sorted_menu_items;
}
$removed_items = [];
// Build list of all items that should be removed (condition fails or parent removed).
// Note: WordPress guarantees menu items are in hierarchical order (parents before children).
foreach ( $sorted_menu_items as $item ) {
$item_id = (int) $item->ID;
// Skip if already marked for removal.
if ( in_array( $item_id, $removed_items, true ) ) {
continue;
}
// Check if parent was removed.
$parent_id = ! empty( $item->menu_item_parent ) ? (int) $item->menu_item_parent : 0;
if ( $parent_id && in_array( $parent_id, $removed_items, true ) ) {
$removed_items[] = $item_id;
continue;
}
// Check condition.
$condition_id = get_post_meta( $item_id, '_gb_menu_condition', true );
if ( $condition_id && ! $this->should_show_menu_item( $item_id, $condition_id ) ) {
$removed_items[] = $item_id;
}
}
// If no items were removed, return the original array.
if ( empty( $removed_items ) ) {
return $sorted_menu_items;
}
// Filter out removed items.
$final_items = [];
foreach ( $sorted_menu_items as $item ) {
if ( ! in_array( (int) $item->ID, $removed_items, true ) ) {
$final_items[] = $item;
}
}
return $final_items;
}
/**
* Check if a menu item should be shown based on its condition.
*
* @param int $menu_item_id The menu item ID.
* @param string $condition_id The condition ID.
* @return bool Whether to show the menu item.
*/
private function should_show_menu_item( $menu_item_id, $condition_id ) {
$condition_id = absint( $condition_id );
if ( ! $condition_id ) {
return true;
}
$invert_condition = get_post_meta( $menu_item_id, '_gb_menu_condition_invert', true ) === '1';
// Check cache first to avoid repeated evaluations.
$cache_key = $condition_id . '_' . ( $invert_condition ? '1' : '0' );
if ( isset( $this->condition_cache[ $cache_key ] ) ) {
return $this->condition_cache[ $cache_key ];
}
// Default to showing the menu item.
$show = true;
// Check if the condition post exists and is published.
$condition_post = get_post( $condition_id );
if ( $condition_post && 'publish' === $condition_post->post_status ) {
// Get the condition data.
$display_conditions = get_post_meta( $condition_id, '_gb_conditions', true );
if ( ! empty( $display_conditions ) ) {
// Use the existing conditions system to evaluate.
$show = GenerateBlocks_Pro_Conditions::show( $display_conditions );
// If invert is enabled, flip the result.
if ( $invert_condition ) {
$show = ! $show;
}
}
}
// Cache the result.
$this->condition_cache[ $cache_key ] = $show;
return $show;
}
/**
* Add menu item conditions handler to usage search.
*
* @param array $handlers Existing handlers.
* @param int $condition_id The condition ID.
* @return array Modified handlers.
*/
public function add_menu_usage_handler( $handlers, $condition_id ) {
$handlers['menu_item_conditions'] = [
'method' => 'search_menu_item_conditions_usage',
'label' => __( 'Menu Item Conditions', 'generateblocks-pro' ),
];
return $handlers;
}
/**
* AJAX handler for searching conditions.
*/
public function ajax_search_conditions() {
// Verify nonce.
if ( ! wp_verify_nonce( $_GET['_ajax_nonce'], 'gb_search_conditions' ) ) {
wp_die();
}
$search = isset( $_GET['search'] ) ? sanitize_text_field( $_GET['search'] ) : '';
$page = isset( $_GET['page'] ) ? absint( $_GET['page'] ) : 1;
$per_page = $this->get_conditions_limit();
$args = [
'post_type' => 'gblocks_condition',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
'post_status' => 'publish',
];
if ( $search ) {
$args['s'] = $search;
$args['orderby'] = 'relevance';
}
$conditions = get_posts( $args );
$total = wp_count_posts( 'gblocks_condition' )->publish;
$results = [];
foreach ( $conditions as $condition ) {
$results[] = [
'id' => $condition->ID,
'text' => $condition->post_title ? $condition->post_title : __( 'Untitled Condition', 'generateblocks-pro' ),
];
}
wp_send_json(
[
'results' => $results,
'pagination' => [
'more' => ( $page * $per_page ) < $total,
],
]
);
}
/**
* AJAX handler for loading more conditions.
*/
public function ajax_load_more_conditions() {
// Verify nonce.
if ( ! wp_verify_nonce( $_GET['_ajax_nonce'], 'gb_load_more_conditions' ) ) {
wp_die();
}
$page = isset( $_GET['page'] ) ? absint( $_GET['page'] ) : 2;
$limit = $this->get_conditions_limit();
$offset = ( $page - 1 ) * $limit;
$conditions = get_posts(
[
'post_type' => 'gblocks_condition',
'posts_per_page' => $limit,
'offset' => $offset,
'orderby' => 'date',
'order' => 'DESC',
'post_status' => 'publish',
'suppress_filters' => false,
'no_found_rows' => false,
]
);
$formatted_conditions = [];
foreach ( $conditions as $condition ) {
$formatted_conditions[] = [
'id' => $condition->ID,
'title' => $condition->post_title ? $condition->post_title : __( 'Untitled Condition', 'generateblocks-pro' ),
];
}
$total = wp_count_posts( 'gblocks_condition' )->publish;
$has_more = ( $page * $limit ) < $total;
wp_send_json_success(
[
'conditions' => $formatted_conditions,
'has_more' => $has_more,
]
);
}
/**
* Add admin scripts for dynamic UI behavior.
*
* @param string $hook_suffix The current admin page.
*/
public function add_admin_scripts( $hook_suffix ) {
// Only load on nav-menus.php page.
if ( 'nav-menus.php' !== $hook_suffix ) {
return;
}
wp_enqueue_script(
'gb-menu-item-conditions',
GENERATEBLOCKS_PRO_DIR_URL . 'dist/menu-item-conditions.js',
array(),
GENERATEBLOCKS_PRO_VERSION,
true
);
wp_localize_script(
'gb-menu-item-conditions',
'gbMenuConditions',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonces' => array(
'loadMore' => wp_create_nonce( 'gb_load_more_conditions' ),
'search' => wp_create_nonce( 'gb_search_conditions' ),
),
'strings' => array(
'loading' => __( 'Loading...', 'generateblocks-pro' ),
'loadMore' => __( 'Load More', 'generateblocks-pro' ),
),
)
);
}
}
// Initialize the class.
GenerateBlocks_Pro_Menu_Item_Conditions::get_instance()->init();
@@ -0,0 +1,79 @@
<?php
/**
* The class for related author.
*
* @package Generateblocks/Extend/QueryLoop
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The class for related author.
*
* @since 1.3.0
*/
class GenerateBlocks_Pro_Related_Author extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
public function __construct() {
parent::__construct();
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Author::include_current_author' );
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Author::exclude_current_author' );
}
/**
* Include posts of current post author to the query loop.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public static function include_current_author( $query_args ) {
return self::add_current_author( $query_args, 'author__in' );
}
/**
* Exclude posts of current post author to the query loop.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public static function exclude_current_author( $query_args ) {
return self::add_current_author( $query_args, 'author__not_in' );
}
/**
* Include current author to a query argument.
*
* @since 1.3.0
* @param array $query_args The query arguments.
* @param string $key The query argument key.
*
* @return array The query arguments.
*/
protected static function add_current_author( $query_args, $key ) {
if (
isset( $query_args[ $key ] ) &&
in_array( 'current-post-author', $query_args[ $key ] )
) {
$current_post_author_index = array_search( 'current-post-author', $query_args[ $key ] );
array_splice( $query_args[ $key ], $current_post_author_index, 1 );
if ( ! in_array( get_the_author_meta( 'ID' ), $query_args[ $key ] ) ) {
$query_args[ $key ][] = get_the_author_meta( 'ID' );
}
}
return $query_args;
}
}
GenerateBlocks_Pro_Related_Author::get_instance();
@@ -0,0 +1,80 @@
<?php
/**
* The class for related parent.
*
* @package Generateblocks/Extend/QueryLoop
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The class for related parent.
*
* @since 1.3.0
*/
class GenerateBlocks_Pro_Related_Parent extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
public function __construct() {
parent::__construct();
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Parent::include_current_post' );
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Parent::exclude_current_post' );
}
/**
* Include current post as parent to the query loop.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public static function include_current_post( $query_args ) {
return self::add_current_post( $query_args, 'post_parent__in' );
}
/**
* Exclude current post as parent to the query loop.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public static function exclude_current_post( $query_args ) {
return self::add_current_post( $query_args, 'post_parent__not_in' );
}
/**
* Include current post to a query argument.
*
* @since 1.3.0
* @param array $query_args The query arguments.
* @param string $key The query argument key.
*
* @return array The query arguments.
*/
protected static function add_current_post( $query_args, $key ) {
if (
isset( $query_args[ $key ] ) &&
in_array( 'current-post', $query_args[ $key ] ) &&
get_post_type() === $query_args['post_type']
) {
$current_post_index = array_search( 'current-post', $query_args[ $key ] );
array_splice( $query_args[ $key ], $current_post_index, 1 );
if ( ! in_array( get_the_ID(), $query_args[ $key ] ) ) {
$query_args[ $key ][] = get_the_ID();
}
}
return $query_args;
}
}
GenerateBlocks_Pro_Related_Parent::get_instance();
@@ -0,0 +1,63 @@
<?php
/**
* The class for related post.
*
* @package Generateblocks/Extend/QueryLoop
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The related posts class.
*
* @since 1.3.0
*/
class GenerateBlocks_Pro_Related_Post extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
public function __construct() {
parent::__construct();
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Post::exclude_current_post' );
}
/**
* Exclude current post from the query loop.
*
* @since 1.3.0
* @param Array $query_args The query arguments.
*
* @return Array The query arguments without current post.
*/
public static function exclude_current_post( $query_args ) {
if (
isset( $query_args['post__not_in'] ) &&
in_array( 'exclude-current', $query_args['post__not_in'] ) &&
get_post_type() === $query_args['post_type']
) {
if ( ! in_array( get_the_ID(), $query_args['post__not_in'] ) ) {
$query_args['post__not_in'][] = get_the_ID();
}
$exclude_current_index = array_search( 'exclude-current', $query_args['post__not_in'] );
array_splice( $query_args['post__not_in'], $exclude_current_index, 1 );
// This is to avoid current post being dynamically added to post__in which will show him in the result set.
if (
isset( $query_args['post__in'] ) &&
in_array( get_the_ID(), $query_args['post__in'] )
) {
$current_post_index = array_search( get_the_ID(), $query_args['post__in'] );
array_splice( $query_args['post__in'], $current_post_index, 1 );
}
}
return $query_args;
}
}
GenerateBlocks_Pro_Related_Post::get_instance();
@@ -0,0 +1,96 @@
<?php
/**
* The class for related terms.
*
* @package Generateblocks/Extend/QueryLoop
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The class for related terms.
*
* @since 1.3.0
*/
class GenerateBlocks_Pro_Related_Terms extends GenerateBlocks_Pro_Singleton {
/**
* The class constructor.
*/
public function __construct() {
parent::__construct();
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Terms::include_current_post_terms' );
add_filter( 'generateblocks_query_loop_args', 'GenerateBlocks_Pro_Related_Terms::exclude_current_post_terms' );
}
/**
* Include current post terms to the query loop.
*
* @since 1.3.0
* @param Array $query_args The query arguments.
*
* @return Array The query arguments with current post terms.
*/
public static function include_current_post_terms( $query_args ) {
return self::add_current_post_terms( $query_args, 'tax_query' );
}
/**
* Exclude current post terms to the query loop.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments without current post terms.
*/
public static function exclude_current_post_terms( $query_args ) {
return self::add_current_post_terms( $query_args, 'tax_query_exclude' );
}
/**
* Include current post term to a query argument.
*
* @since 1.3.0
* @param array $query_args The query arguments.
* @param string $key The query argument key.
*
* @return array The query arguments.
*/
protected static function add_current_post_terms( $query_args, $key ) {
if (
is_singular() &&
isset( $query_args[ $key ] )
) {
$query_args[ $key ] = array_map(
function( $tax ) {
if ( isset( $tax['terms'] ) && in_array( 'current-terms', $tax['terms'] ) ) {
$registered_taxonomies = get_object_taxonomies( get_post_type() );
if ( in_array( $tax['taxonomy'], $registered_taxonomies ) ) {
$related_terms = wp_get_object_terms(
get_the_ID(),
$tax['taxonomy'],
array( 'fields' => 'ids' )
);
$tax['terms'] = array_merge( $tax['terms'], $related_terms );
}
$current_terms_index = array_search( 'current-terms', $tax['terms'] );
array_splice( $tax['terms'], $current_terms_index, 1 );
}
return $tax;
},
$query_args[ $key ]
);
}
return $query_args;
}
}
GenerateBlocks_Pro_Related_Terms::get_instance();
@@ -0,0 +1,311 @@
<?php
/**
* Extend the Query block.
*
* @package GenerateBlocksPro\Extend\Query
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Extend the default Query block.
*
* @since 2.0.0
*/
class GenerateBlocks_Pro_Block_Query extends GenerateBlocks_Pro_Singleton {
// Add new Query types here.
const TYPE_POST_META = 'post_meta';
const TYPE_OPTION = 'option';
/**
* Init function.
*/
public function init() {
if ( ! class_exists( 'GenerateBlocks_Meta_Handler' ) ) {
return;
}
add_filter( 'generateblocks_query_data', [ $this, 'set_query_data' ], 10, 5 );
add_filter( 'generateblocks_dynamic_tag_id', [ $this, 'set_dynamic_tag_id' ], 10, 3 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'exclude_current_post' ], 10, 4 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'include_current_author' ], 10, 4 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'exclude_current_author' ], 10, 4 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'current_post_terms' ], 10, 4 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'include_current_parent' ], 10, 4 );
add_filter( 'generateblocks_query_wp_query_args', [ $this, 'exclude_current_parent' ], 10, 4 );
}
/**
* Update the dynamic tag's ID if it's a post tag, it has a valid loop item ID, and no other ID is set in options.
*
* @param int $id The current ID value for the tag.
* @param array $options The tag options.
* @param object $instance The block instance for the block containing the tag.
* @return int The ID for the dynamic tag.
*/
public function set_dynamic_tag_id( $id, $options, $instance ) {
// If an ID is set in options, use the original ID.
if ( $options['id'] ?? false ) {
return $id;
}
$loop_item = $instance->context['generateblocks/loopItem'] ?? null;
if ( ! $loop_item ) {
return $id;
}
// Look for the ID or id keys and return the original $id if none can be found.
if ( is_array( $loop_item ) ) {
return $loop_item['ID'] ?? $loop_item['id'] ?? $id;
} elseif ( is_object( $loop_item ) ) {
return $loop_item->ID ?? $loop_item->id ?? $id;
}
return $id;
}
/**
* Set the query data for certain types.
*
* @param array $query_data The current query data.
* @param string $query_type The type of query.
* @param array $attributes An array of block attributes.
* @param WP_Block $block The block instance.
* @param int $page The page number.
*
* @return array An array of query data.
*/
public function set_query_data( $query_data, $query_type, $attributes, $block, $page ) {
$pro_types = [
self::TYPE_POST_META,
self::TYPE_OPTION,
];
if ( ! in_array( $query_type, $pro_types, true ) ) {
return $query_data;
}
$query = $attributes['query'] ?? [];
$id = $query['meta_key_id'] ?? '';
$meta_key = $query['meta_key'] ?? '';
if ( 'current' === $id || ! $id ) {
$id = get_the_ID();
}
if ( self::TYPE_POST_META === $query_type ) {
$value = GenerateBlocks_Meta_Handler::get_post_meta( $id, $meta_key, false );
} elseif ( self::TYPE_OPTION === $query_type ) {
$value = GenerateBlocks_Meta_Handler::get_option( $meta_key, false );
}
$data = is_array( $value ) ? $value : [];
// Handle pagination.
$posts_per_page = (int) ( $query['posts_per_page'] ?? get_option( 'posts_per_page' ) );
$offset = isset( $query['offset'] ) && is_numeric( $query['offset'] ) ? $query['offset'] : 0;
// Get the total number of items less the user specified offset.
$data = array_slice( $data, $offset );
$max_pages = $posts_per_page > 0
? (int) ceil( count( $data ) / $posts_per_page )
: 1;
if ( 0 === $posts_per_page ) {
$max_pages = 0;
}
return [
'data' => $data,
'no_results' => empty( $data ),
'args' => $query,
'max_num_pages' => $max_pages,
];
}
/**
* Exclude current post from the query.
*
* @param array $query_args The query arguments.
* @param array $attributes The block attributes.
* @param WP_Block|null $block The block instance.
* @param array $current The current post data.
*
* @return array The query arguments without current post.
*/
public function exclude_current_post( $query_args, $attributes, $block, $current ) {
$current_post_id = $current['post_id'] ?? get_the_ID();
return self::add_current_post( $query_args, $current_post_id, 'post__not_in' );
}
/**
* Include current parent post from the query.
*
* @param array $query_args The query arguments.
* @param array $attributes The block attributes.
* @param WP_Block|null $block The block instance.
* @param array $current The current post data.
*
* @return array The query arguments without current post.
*/
public function include_current_parent( $query_args, $attributes, $block, $current ) {
$current_post_id = $current['post_id'] ?? get_the_ID();
return self::add_current_post( $query_args, $current_post_id, 'post_parent__in' );
}
/**
* Exclude current parent post from the query.
*
* @param array $query_args The query arguments.
* @param array $attributes The block attributes.
* @param WP_Block|null $block The block instance.
* @param array $current The current post data.
*
* @return array The query arguments without current post.
*/
public function exclude_current_parent( $query_args, $attributes, $block, $current ) {
$current_post_id = $current['post_id'] ?? get_the_ID();
return self::add_current_post( $query_args, $current_post_id, 'post_parent__not_in' );
}
/**
* Adds the current post ID to the query args.
*
* @param array $query_args The query arguments.
* @param int $current_post_id The current post ID.
* @param string $key The key to check.
*
* @return array The query arguments without current post.
*/
public static function add_current_post( $query_args, $current_post_id, $key ) {
if (
isset( $query_args[ $key ] ) &&
in_array( 'current', $query_args[ $key ] )
) {
if ( ! in_array( $current_post_id, $query_args[ $key ] ) ) {
$query_args[ $key ][] = $current_post_id;
}
$exclude_current_index = array_search( 'current', $query_args[ $key ] );
array_splice( $query_args[ $key ], $exclude_current_index, 1 );
if ( 'post__not_in' === $key ) {
// This is to avoid current post being dynamically added to post__in which will show him in the result set.
if (
isset( $query_args['post__in'] ) &&
in_array( $current_post_id, $query_args['post__in'] )
) {
$current_post_index = array_search( $current_post_id, $query_args['post__in'] );
array_splice( $query_args['post__in'], $current_post_index, 1 );
}
}
}
return $query_args;
}
/**
* Include posts of current post author to the query.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public function include_current_author( $query_args ) {
return self::add_current_author( $query_args, 'author__in' );
}
/**
* Exclude posts of current post author to the query.
*
* @since 1.3.0
* @param array $query_args The query arguments.
*
* @return array The query arguments.
*/
public function exclude_current_author( $query_args ) {
return self::add_current_author( $query_args, 'author__not_in' );
}
/**
* Include current author to a query argument.
*
* @since 1.3.0
* @param array $query_args The query arguments.
* @param string $key The query argument key.
*
* @return array The query arguments.
*/
public function add_current_author( $query_args, $key ) {
if (
isset( $query_args[ $key ] ) &&
in_array( 'current', $query_args[ $key ] )
) {
$current_post_author_index = array_search( 'current', $query_args[ $key ] );
array_splice( $query_args[ $key ], $current_post_author_index, 1 );
if ( ! in_array( get_the_author_meta( 'ID' ), $query_args[ $key ] ) ) {
$query_args[ $key ][] = get_the_author_meta( 'ID' );
}
}
return $query_args;
}
/**
* Process the "current" post terms.
*
* @param array $query_args The query arguments.
* @param array $attributes The block attributes.
* @param WP_Block|null $block The block instance.
* @param array $current The current post data.
*
* @return array The query arguments.
*/
public function current_post_terms( $query_args, $attributes, $block, $current ) {
if (
$current['post_id'] &&
isset( $query_args['tax_query'] )
) {
$query_args['tax_query'] = array_map(
function( $tax ) use ( $current ) {
if ( ! isset( $tax['terms'] ) ) {
return $tax;
}
if ( in_array( 'current', $tax['terms'], true ) ) {
$registered_taxonomies = get_object_taxonomies( get_post_type( $current['post_id'] ) );
if ( in_array( $tax['taxonomy'], $registered_taxonomies, true ) ) {
$related_terms = wp_get_object_terms(
$current['post_id'],
$tax['taxonomy'],
array( 'fields' => 'ids' )
);
$tax['terms'] = array_merge( $tax['terms'], $related_terms );
}
$current_terms_index = array_search( 'current', $tax['terms'] );
array_splice( $tax['terms'], $current_terms_index, 1 );
}
return $tax;
},
$query_args['tax_query']
);
}
return $query_args;
}
}
GenerateBlocks_Pro_Block_Query::get_instance()->init();
@@ -0,0 +1,38 @@
<?php
/**
* Feature settings integration for Pro features.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
add_filter( 'generateblocks_option_defaults', 'generateblocks_pro_add_feature_defaults' );
/**
* Add feature settings to GenerateBlocks option defaults.
*
* @param array $defaults The existing defaults.
* @return array Modified defaults.
*/
function generateblocks_pro_add_feature_defaults( $defaults ) {
$defaults['enable_overlay_panels'] = true;
$defaults['enable_block_conditions'] = true;
$defaults['enable_forms'] = false;
return $defaults;
}
add_filter( 'generateblocks_option_sanitize_callbacks', 'generateblocks_pro_add_feature_sanitize' );
/**
* Add sanitization callbacks for feature settings.
*
* @param array $callbacks The existing callbacks.
* @return array Modified callbacks.
*/
function generateblocks_pro_add_feature_sanitize( $callbacks ) {
$callbacks['enable_overlay_panels'] = 'rest_sanitize_boolean';
$callbacks['enable_block_conditions'] = 'rest_sanitize_boolean';
$callbacks['enable_forms'] = 'rest_sanitize_boolean';
return $callbacks;
}
@@ -0,0 +1,108 @@
<?php
/**
* Boot the optional Forms module.
*
* Nothing in this file is loaded unless Forms is enabled. Keep all Forms
* class loading, hooks, routes, post types, and provider registration behind
* this boundary so disabled Forms stay out of the request lifecycle.
*
* @package GenerateBlocks Pro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-sanitizer.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-spam.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-turnstile.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-http.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-action-registry.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-integration-registry.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-action-email.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-action-email-signup.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/integrations/builtin-loader.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-action-webhook.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-action-confirmation-email.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-post-type.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-schema-builder.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-render.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-preview-rest.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-processor.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-rest.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-integration-rest.php';
require_once GENERATEBLOCKS_PRO_DIR . 'includes/form/class-form-submissions.php';
// Initialize form system on 'init' so third-party code can hook in.
// Third-party form integrations can register on the
// generateblocks_form_register_integrations hook.
add_action( 'init', 'generateblocks_pro_init_form_system' );
// Hide the form no-JS fallback inside pattern-library preview thumbnails.
add_action( 'wp_head', 'generateblocks_pro_form_hide_preview_noscript' );
/**
* Initialize the form system.
*/
function generateblocks_pro_init_form_system() {
/**
* Register email integration API keys.
*
* Use generateblocks_pro_set_email_integration() inside this hook
* to provide API keys for email marketing services.
*
* @since 2.6.0
*
* @example
* add_action( 'generateblocks_form_register_email_integrations', function() {
* generateblocks_pro_set_email_integration( 'mailchimp', 'your-api-key' );
* } );
*/
do_action( 'generateblocks_form_register_email_integrations' );
GenerateBlocks_Pro_Form_Action_Email::init();
GenerateBlocks_Pro_Form_Action_Email_Signup::init();
GenerateBlocks_Pro_Form_Action_Webhook::init();
GenerateBlocks_Pro_Form_Action_Confirmation_Email::init();
GenerateBlocks_Pro_Form_Turnstile::init();
/**
* Register custom form integration providers.
*
* Use generateblocks_pro_register_form_integration() inside this hook to
* expose a provider in the form editor.
* This runs after built-ins, so registering an existing ID intentionally
* replaces the built-in definition for this request.
*
* @since 2.6.0
*/
do_action( 'generateblocks_form_register_integrations' );
GenerateBlocks_Pro_Form_Post_Type::get_instance()->init();
GenerateBlocks_Pro_Form_Schema_Builder::get_instance()->init();
GenerateBlocks_Pro_Form_Preview_Rest::get_instance()->init();
GenerateBlocks_Pro_Form_Rest::get_instance()->init();
GenerateBlocks_Pro_Form_Integration_Rest::get_instance()->init();
GenerateBlocks_Pro_Form_Submissions::get_instance()->init();
}
/**
* Suppress the form no-JS fallback inside pattern-library preview thumbnails.
*
* Pattern previews load the form into a sandboxed iframe with no `allow-scripts`
* (the `?gb-template-viewer=1` shell), so with scripting disabled the form's
* <noscript> fallback renders in the thumbnail. Emitting this style only on that
* request keeps the message intact for genuine no-JS visitors on real pages.
*
* The preview iframe always loads the viewer via the `gb-template-viewer` query
* string, so a presence check on $_GET is enough — and reading the superglobal
* never touches $wp_query, so there is no query-not-ready fatal to guard.
*/
function generateblocks_pro_form_hide_preview_noscript() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only existence check, emits a static style and changes no state.
if ( ! isset( $_GET['gb-template-viewer'] ) ) {
return;
}
echo '<style id="gb-pro-form-preview-noscript">.gb-form noscript{display:none}</style>';
}
@@ -0,0 +1,391 @@
<?php
/**
* Confirmation email form action.
*
* Sends an auto-reply to the person who submitted the form.
* Plain text only. Never uses user data in the From header.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Confirmation email form action class.
*/
class GenerateBlocks_Pro_Form_Action_Confirmation_Email {
/**
* Register the action with the registry.
*/
public static function init() {
GenerateBlocks_Pro_Form_Action_Registry::register( 'confirmation-email', [ __CLASS__, 'send' ] );
}
/**
* Validate confirmation email settings before publishing or delivery.
*
* @param array $form_settings The form block settings.
* @param array|null $schema Optional derived field schema list.
* @return true|WP_Error
*/
public static function validate_settings( $form_settings, $schema = null ) {
if ( is_array( $schema ) ) {
$email_field = self::get_email_field( $form_settings );
if ( '' === $email_field ) {
return new WP_Error(
'gb_form_confirmation_email_field_missing',
__( 'Confirmation email field is required.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
$field = self::find_schema_field( $schema, $email_field );
if ( ! $field ) {
return new WP_Error(
'gb_form_confirmation_email_field_missing',
sprintf(
/* translators: %s: email field name. */
__( 'Confirmation email field "%s" does not exist.', 'generateblocks-pro' ),
$email_field
),
[ 'status' => 400 ]
);
}
$field_type = $field['type'] ?? '';
if ( ! in_array( $field_type, [ 'email', 'text' ], true ) ) {
return new WP_Error(
'gb_form_confirmation_email_field_invalid',
sprintf(
/* translators: %s: email field name. */
__( 'Confirmation email field "%s" must be an email or text field.', 'generateblocks-pro' ),
$email_field
),
[ 'status' => 400 ]
);
}
}
return true;
}
/**
* Validate the submitted confirmation email recipient before delivery.
*
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings The form block settings.
* @return true|WP_Error
*/
public static function validate_submission( $sanitized_data, $form_settings ) {
$email_field = self::get_email_field( $form_settings );
if ( '' === $email_field ) {
return new WP_Error(
'missing_email_field',
'Confirmation email field is not configured.',
[ 'status' => 400 ]
);
}
// is_array catches both missing key and shape mutation by a
// generateblocks_form_validate_submission filter that returned
// a flat scalar instead of the [value, label, type] array.
$field = $sanitized_data[ $email_field ] ?? null;
if ( ! is_array( $field ) ) {
return new WP_Error(
'missing_email_field',
'Confirmation email field not found in submission.',
[ 'status' => 400 ]
);
}
$to = sanitize_email( (string) ( $field['value'] ?? '' ) );
if ( ! is_email( $to ) ) {
return new WP_Error(
'invalid_recipient',
'Invalid recipient email for confirmation.',
[ 'status' => 400 ]
);
}
return true;
}
/**
* Send a confirmation email to the submitter.
*
* @param array $sanitized_data The sanitized form data (field_name => ['value' => ..., 'label' => ..., 'type' => ...]).
* @param array $form_settings The form block settings.
* @param array $context Request context (post_id, page_url).
* @return true|WP_Error True on success, WP_Error on failure.
*/
public static function send( $sanitized_data, $form_settings, $context = [] ) {
$validation = self::validate_submission( $sanitized_data, $form_settings );
if ( is_wp_error( $validation ) ) {
return $validation;
}
$email_field = self::get_email_field( $form_settings );
$to_field = $sanitized_data[ $email_field ] ?? null;
$to = sanitize_email( is_array( $to_field ) ? (string) ( $to_field['value'] ?? '' ) : '' );
// Build subject with merge tag support.
$raw_subject = $form_settings['confirmationEmailSubject'] ?? '';
if ( empty( $raw_subject ) ) {
$raw_subject = sprintf(
/* translators: %s: site name */
__( 'Thank you for contacting %s', 'generateblocks-pro' ),
get_bloginfo( 'name' )
);
}
$subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
self::replace_merge_tags( $raw_subject, $sanitized_data )
);
// Build body with merge tag support.
$raw_body = $form_settings['confirmationEmailBody'] ?? '';
if ( empty( $raw_body ) ) {
$raw_body = __( 'Thank you for your submission. We will get back to you soon.', 'generateblocks-pro' );
}
$body = self::replace_merge_tags( $raw_body, $sanitized_data );
// Plain text only. Never set From from user data.
$headers = [ 'Content-Type: text/plain; charset=UTF-8' ];
$reply_to_header = self::build_reply_to_header(
$form_settings['confirmationEmailReplyToEmail'] ?? '',
$form_settings['confirmationEmailReplyToName'] ?? ''
);
if ( '' !== $reply_to_header ) {
$headers[] = $reply_to_header;
}
/**
* Filter the confirmation email arguments before sending.
*
* @since 2.6.0
* @param array $email_args {
* Email arguments.
*
* @type string $to Recipient email address.
* @type string $subject Email subject line.
* @type string $body Email body content.
* @type array $headers Email headers.
* }
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings The form block settings.
* @param array $context Request context.
*/
$email_args = apply_filters(
'generateblocks_form_confirmation_email_args',
[
'to' => $to,
'subject' => $subject,
'body' => $body,
'headers' => $headers,
],
$sanitized_data,
$form_settings,
$context
);
// Re-validate filter output: defensive against third-party callbacks
// returning values with embedded newlines (header injection) or
// reshaping the array entirely.
if ( ! is_array( $email_args ) ) {
$email_args = [];
}
$raw_to = $email_args['to'] ?? '';
// Two-pass sanitization: strip CR/LF/tab + percent-encoded variants
// before sanitize_email() so a filter that returned an address with
// embedded newlines can't leak into wp_mail's `To` header. Belt and
// suspenders — sanitize_email() rejects most malformed addresses
// already, but this matches how `subject` and `headers` are
// hardened above so the contract is symmetric.
$safe_to = is_string( $raw_to )
? sanitize_email( GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $raw_to ) )
: '';
$raw_subject = $email_args['subject'] ?? '';
$safe_subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
is_string( $raw_subject ) ? $raw_subject : ''
);
$safe_headers = self::sanitize_filter_headers( $email_args['headers'] ?? [] );
$raw_body = $email_args['body'] ?? '';
$safe_body = is_string( $raw_body ) ? $raw_body : '';
if ( ! is_email( $safe_to ) ) {
return new WP_Error( 'confirmation_email_invalid_to', 'No valid email recipient.' );
}
// This email is submitter-facing: with no other sender customization,
// wp_mail sends as core's literal "WordPress". Swap that default for
// the site name, scoped to this send only.
add_filter( 'wp_mail_from_name', [ __CLASS__, 'filter_default_from_name' ], 999 );
try {
$sent = wp_mail(
$safe_to,
$safe_subject,
$safe_body,
$safe_headers
);
} finally {
remove_filter( 'wp_mail_from_name', [ __CLASS__, 'filter_default_from_name' ], 999 );
}
if ( ! $sent ) {
return new WP_Error( 'confirmation_email_failed', 'Failed to send confirmation email.' );
}
return true;
}
/**
* Swap core's literal "WordPress" default From name for the site name.
*
* Runs at priority 999 so any name customized earlier — an SMTP plugin
* or a site-wide wp_mail_from_name filter — passes through untouched;
* only the unmodified core default is replaced.
*
* @param mixed $name Current From display name.
* @return mixed From display name.
*/
public static function filter_default_from_name( $name ) {
if ( 'WordPress' !== $name ) {
return $name;
}
$site_name = trim(
GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES )
)
);
return '' !== $site_name ? $site_name : $name;
}
/**
* Re-validate the `headers` array returned by a filter callback.
*
* Note: we only strip CR/LF/tab and their percent-encoded variants here.
* Running sanitize_text_field() would mangle valid display-name email
* syntax like "Reply-To: Name <a@b.com>" (strip_tags eats the <...>).
*
* @param mixed $headers The filtered headers value.
* @return array Sanitized headers.
*/
private static function sanitize_filter_headers( $headers ) {
if ( ! is_array( $headers ) ) {
return [];
}
$safe = [];
foreach ( $headers as $header ) {
if ( ! is_string( $header ) ) {
continue;
}
$header = preg_replace( '/[\r\n\t]/', '', $header );
$header = str_replace( [ '%0a', '%0d', '%0A', '%0D' ], '', $header );
$header = trim( $header );
if ( '' !== $header ) {
$safe[] = $header;
}
}
return $safe;
}
/**
* Build a safe Reply-To header from static confirmation email settings.
*
* @param mixed $raw_email Configured reply-to email.
* @param mixed $raw_name Configured reply-to display name.
* @return string Header line, or empty string when no valid email exists.
*/
private static function build_reply_to_header( $raw_email, $raw_name ) {
$email = is_scalar( $raw_email ) ? sanitize_email( (string) $raw_email ) : '';
if ( ! is_email( $email ) ) {
return '';
}
$email = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $email );
$name = is_scalar( $raw_name ) ? sanitize_text_field( (string) $raw_name ) : '';
$name = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $name );
$name = trim( str_replace( [ '<', '>', '"', ',', ':', '\\' ], '', $name ) );
if ( '' === $name ) {
return 'Reply-To: ' . $email;
}
return 'Reply-To: ' . $name . ' <' . $email . '>';
}
/**
* Replace {field:name} merge tags with sanitized field values.
*
* Uses sanitize_textarea_field() for textarea fields to preserve newlines,
* and sanitize_text_field() for everything else.
*
* @param string $text The text containing merge tags.
* @param array $sanitized_data The sanitized form data.
* @return string Text with merge tags replaced.
*/
public static function replace_merge_tags( $text, $sanitized_data ) {
return GenerateBlocks_Pro_Form_Sanitizer::replace_merge_tags( $text, $sanitized_data );
}
/**
* Get the configured recipient field.
*
* @param array $form_settings The form block settings.
* @return string
*/
private static function get_email_field( $form_settings ) {
if (
isset( $form_settings['confirmationEmailField'] )
&& ( is_array( $form_settings['confirmationEmailField'] ) || is_object( $form_settings['confirmationEmailField'] ) )
) {
return '';
}
return isset( $form_settings['confirmationEmailField'] )
? sanitize_key( $form_settings['confirmationEmailField'] )
: '';
}
/**
* Find a field in the derived schema by name.
*
* @param array $schema Field schema list.
* @param string $name Field name.
* @return array|null
*/
private static function find_schema_field( $schema, $name ) {
foreach ( $schema as $field ) {
if ( is_array( $field ) && ( $field['name'] ?? '' ) === $name ) {
return $field;
}
}
return null;
}
}
@@ -0,0 +1,397 @@
<?php
/**
* Email signup form action.
*
* Routes a submission to the selected email marketing provider.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Email signup action class.
*/
class GenerateBlocks_Pro_Form_Action_Email_Signup {
/**
* Registered action name.
*/
const ACTION = 'email-signup';
/**
* Register the action with the action registry.
*/
public static function init() {
GenerateBlocks_Pro_Form_Action_Registry::register( self::ACTION, [ __CLASS__, 'handle' ] );
}
/**
* Handle an email signup submission.
*
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings The form block settings.
* @param array $context Request context.
* @return true|WP_Error
*/
public static function handle( $sanitized_data, $form_settings, $context = [] ) {
$settings = self::normalize_settings( self::get_settings( $form_settings ) );
$validation = self::validate_settings( $settings );
if ( is_wp_error( $validation ) ) {
return $validation;
}
$submission_check = self::validate_submission( $sanitized_data, $settings );
if ( is_wp_error( $submission_check ) ) {
return $submission_check;
}
$provider = GenerateBlocks_Pro_Form_Integration_Registry::get_provider( $settings['provider'] );
$callback = $provider['subscribe_callback'] ?? null;
if ( ! is_callable( $callback ) ) {
return new WP_Error( 'email_signup_unavailable', 'Selected email signup provider is unavailable.' );
}
return call_user_func( $callback, $sanitized_data, $settings, $context );
}
/**
* Extract email-signup settings from form config.
*
* @param array $form_settings Form settings.
* @return array
*/
private static function get_settings( $form_settings ) {
return isset( $form_settings['actionSettings'][ self::ACTION ] ) && is_array( $form_settings['actionSettings'][ self::ACTION ] )
? $form_settings['actionSettings'][ self::ACTION ]
: [];
}
/**
* Normalize email-signup settings before validation/dispatch.
*
* @param array $settings Email signup settings.
* @return array
*/
private static function normalize_settings( $settings ) {
$settings = is_array( $settings ) ? $settings : [];
$settings['provider'] = isset( $settings['provider'] ) ? sanitize_key( $settings['provider'] ) : '';
if ( isset( $settings['emailField'] ) && ( is_array( $settings['emailField'] ) || is_object( $settings['emailField'] ) ) ) {
$settings['emailField'] = '';
}
$email_field = isset( $settings['emailField'] ) ? sanitize_key( $settings['emailField'] ) : '';
$settings['emailField'] = '' !== $email_field ? $email_field : 'email';
if ( isset( $settings['nameField'] ) && ( is_array( $settings['nameField'] ) || is_object( $settings['nameField'] ) ) ) {
$settings['nameField'] = '';
}
$settings['nameField'] = isset( $settings['nameField'] ) ? sanitize_key( $settings['nameField'] ) : '';
return $settings;
}
/**
* Validate the provider-level email signup settings.
*
* Provider callbacks still validate provider-specific ID shapes and field
* maps before making external requests.
*
* @param array $settings Email signup settings.
* @param array|null $schema Optional form field schema list.
* @return true|WP_Error
*/
public static function validate_settings( $settings, $schema = null ) {
$settings = self::normalize_settings( $settings );
$provider_id = $settings['provider'];
if ( '' === $provider_id ) {
return new WP_Error(
'gb_form_email_signup_provider_missing',
__( 'Email signup service is required.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
$provider = GenerateBlocks_Pro_Form_Integration_Registry::get_provider( $provider_id );
if ( ! $provider || 'email' !== $provider['type'] ) {
return new WP_Error(
'gb_form_email_signup_provider_invalid',
__( 'Selected email signup service is unavailable.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
if ( ! is_callable( $provider['subscribe_callback'] ?? null ) ) {
return new WP_Error(
'gb_form_email_signup_provider_unavailable',
__( 'Selected email signup service cannot process submissions.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
$connection_check = self::validate_provider_connection( $provider_id, $provider );
if ( is_wp_error( $connection_check ) ) {
return $connection_check;
}
$provider_settings_check = self::validate_provider_settings( $settings, $provider );
if ( is_wp_error( $provider_settings_check ) ) {
return $provider_settings_check;
}
$destination = $settings['destinationId'] ?? '';
if (
! empty( $provider['destination']['required'] )
&& ( is_array( $destination ) || is_object( $destination ) || '' === trim( (string) $destination ) )
) {
return new WP_Error(
'gb_form_email_signup_destination_missing',
sprintf(
/* translators: %s: destination label, e.g. Audience/List/Form. */
__( 'Email signup %s is required.', 'generateblocks-pro' ),
$provider['destination']['label'] ? strtolower( $provider['destination']['label'] ) : __( 'destination', 'generateblocks-pro' )
),
[ 'status' => 400 ]
);
}
$secondary_destination = $settings['secondaryDestinationId'] ?? '';
if (
! empty( $provider['secondary_destination']['required'] )
&& ( is_array( $secondary_destination ) || is_object( $secondary_destination ) || '' === trim( (string) $secondary_destination ) )
) {
return new WP_Error(
'gb_form_email_signup_secondary_destination_missing',
sprintf(
/* translators: %s: secondary destination label, e.g. Tag. */
__( 'Email signup %s is required.', 'generateblocks-pro' ),
$provider['secondary_destination']['label'] ? strtolower( $provider['secondary_destination']['label'] ) : __( 'secondary destination', 'generateblocks-pro' )
),
[ 'status' => 400 ]
);
}
if ( is_array( $schema ) ) {
$email_field = $settings['emailField'];
$field = self::find_schema_field( $schema, $email_field );
if ( ! $field ) {
return new WP_Error(
'gb_form_email_signup_email_field_missing',
sprintf(
/* translators: %s: email field name. */
__( 'Email signup email field "%s" does not exist.', 'generateblocks-pro' ),
$email_field
),
[ 'status' => 400 ]
);
}
$field_type = $field['type'] ?? '';
if ( ! in_array( $field_type, [ 'email', 'text' ], true ) ) {
return new WP_Error(
'gb_form_email_signup_email_field_invalid',
sprintf(
/* translators: %s: email field name. */
__( 'Email signup email field "%s" must be an email or text field.', 'generateblocks-pro' ),
$email_field
),
[ 'status' => 400 ]
);
}
if ( ! empty( $provider['supports_name_field'] ) && '' !== $settings['nameField'] ) {
$name_field = self::find_schema_field( $schema, $settings['nameField'] );
if ( ! $name_field ) {
return new WP_Error(
'gb_form_email_signup_name_field_missing',
sprintf(
/* translators: %s: name field name. */
__( 'Email signup name field "%s" does not exist.', 'generateblocks-pro' ),
$settings['nameField']
),
[ 'status' => 400 ]
);
}
if ( 'text' !== ( $name_field['type'] ?? '' ) ) {
return new WP_Error(
'gb_form_email_signup_name_field_invalid',
sprintf(
/* translators: %s: name field name. */
__( 'Email signup name field "%s" must be a text field.', 'generateblocks-pro' ),
$settings['nameField']
),
[ 'status' => 400 ]
);
}
}
}
return true;
}
/**
* Validate provider-specific email signup settings.
*
* @param array $settings Email signup settings.
* @param array $provider Provider definition.
* @return true|WP_Error
*/
private static function validate_provider_settings( $settings, $provider ) {
foreach ( $provider['settings_fields'] ?? [] as $field ) {
if ( empty( $field['required'] ) ) {
continue;
}
$key = $field['key'] ?? '';
$value = '' !== $key ? ( $settings[ $key ] ?? '' ) : '';
if ( is_array( $value ) || is_object( $value ) || '' === trim( (string) $value ) ) {
return new WP_Error(
'gb_form_email_signup_setting_missing',
sprintf(
/* translators: %s: setting label. */
__( 'Email signup %s is required.', 'generateblocks-pro' ),
$field['label'] ? strtolower( $field['label'] ) : __( 'setting', 'generateblocks-pro' )
),
[ 'status' => 400 ]
);
}
}
if ( is_callable( $provider['settings_validate_callback'] ?? null ) ) {
$result = call_user_func( $provider['settings_validate_callback'], $settings );
if ( is_wp_error( $result ) ) {
return $result;
}
}
return true;
}
/**
* Validate the submitted email signup data before provider dispatch.
*
* @param array $sanitized_data The sanitized form data.
* @param array $settings Email signup settings.
* @return true|WP_Error
*/
public static function validate_submission( $sanitized_data, $settings ) {
$settings = self::normalize_settings( $settings );
$email_field = $settings['emailField'];
if ( ! isset( $sanitized_data[ $email_field ] ) ) {
return new WP_Error(
'gb_form_email_signup_email_missing',
__( 'Email signup requires a valid email address.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
$email = (string) ( $sanitized_data[ $email_field ]['value'] ?? '' );
if ( ! is_email( $email ) ) {
return new WP_Error(
'gb_form_email_signup_email_invalid',
__( 'Email signup requires a valid email address.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
return true;
}
/**
* Validate that the selected provider is connected before publishing or
* dispatching. Provider-specific handlers still validate request payloads.
*
* @param string $provider_id Provider ID.
* @param array $provider Provider definition.
* @return true|WP_Error
*/
private static function validate_provider_connection( $provider_id, $provider ) {
$connection = $provider['connection'] ?? [];
if ( empty( $connection ) || 'none' === ( $connection['type'] ?? 'none' ) ) {
return true;
}
if ( is_callable( $connection['connected_callback'] ?? null ) ) {
$result = call_user_func( $connection['connected_callback'], $provider_id );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'gb_form_email_signup_provider_not_connected',
$result->get_error_message(),
[ 'status' => 400 ]
);
}
if ( ! $result ) {
return new WP_Error(
'gb_form_email_signup_provider_not_connected',
__( 'Selected email signup service is not connected.', 'generateblocks-pro' ),
[ 'status' => 400 ]
);
}
}
foreach ( $connection['fields'] ?? [] as $field ) {
if ( empty( $field['required'] ) ) {
continue;
}
$value = GenerateBlocks_Pro_Form_Integration_Rest::get_connection_setting_for( $provider_id, $field['key'] );
if ( '' === trim( (string) $value ) ) {
return new WP_Error(
'gb_form_email_signup_provider_not_connected',
sprintf(
/* translators: %s: connection setting label */
__( 'Email signup service is not connected. %s is required.', 'generateblocks-pro' ),
$field['label']
),
[ 'status' => 400 ]
);
}
}
return true;
}
/**
* Find a field in the derived schema by name.
*
* @param array $schema Field schema list.
* @param string $name Field name.
* @return array|null
*/
private static function find_schema_field( $schema, $name ) {
foreach ( $schema as $field ) {
if ( is_array( $field ) && ( $field['name'] ?? '' ) === $name ) {
return $field;
}
}
return null;
}
}
@@ -0,0 +1,772 @@
<?php
/**
* Email form action.
*
* Sends form submissions via wp_mail(). Never uses user data in the From email
* address; From name may use sanitized merge tags.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Email form action class.
*/
class GenerateBlocks_Pro_Form_Action_Email {
/**
* Hard cap on recipient count per submission. emailTo is admin-controlled
* (manage_options to write form meta), so submitters can't reach this. The
* cap protects against misconfiguration — pasting a giant CSV — amplifying
* outbound mail per submission.
*/
const MAX_RECIPIENTS = 10;
/**
* Stored value that explicitly disables the Reply-To header.
*/
const REPLY_TO_NONE = '__none';
/**
* Register the email action with the registry.
*/
public static function init() {
GenerateBlocks_Pro_Form_Action_Registry::register( 'email', [ __CLASS__, 'send' ] );
}
/**
* Validate the email action settings before delivery starts.
*
* @param array $form_settings The form block settings.
* @return true|WP_Error
*/
public static function validate_settings( $form_settings ) {
$to = self::parse_recipients(
! empty( $form_settings['emailTo'] ) ? $form_settings['emailTo'] : get_option( 'admin_email' )
);
if ( empty( $to ) ) {
return new WP_Error( 'invalid_recipient', 'Invalid email recipient.' );
}
return true;
}
/**
* Send the form submission via email.
*
* @param array $sanitized_data The sanitized form data (field_name => ['value' => ..., 'label' => ..., 'type' => ...]).
* @param array $form_settings The form block settings.
* @param array $context Request context (post_id, page_url).
* @return true|WP_Error True on success, WP_Error on failure.
*/
public static function send( $sanitized_data, $form_settings, $context = [] ) {
$validation = self::validate_settings( $form_settings );
if ( is_wp_error( $validation ) ) {
return $validation;
}
// Determine recipients — supports comma-separated addresses.
$to = self::parse_recipients(
! empty( $form_settings['emailTo'] ) ? $form_settings['emailTo'] : get_option( 'admin_email' )
);
// Build subject with site name for context.
$site_name = get_bloginfo( 'name' );
if (
isset( $form_settings['emailSubject'] )
&& is_scalar( $form_settings['emailSubject'] )
&& '' !== (string) $form_settings['emailSubject']
) {
$subject = (string) $form_settings['emailSubject'];
} else {
$subject = sprintf(
/* translators: %s: site name */
__( '[%s] New Form Submission', 'generateblocks-pro' ),
$site_name
);
}
$subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
GenerateBlocks_Pro_Form_Sanitizer::replace_merge_tags( $subject, $sanitized_data )
);
/**
* Filter whether admin notification emails should use the built-in HTML template.
*
* @since 2.6.0
* @param bool $send_html Whether to send HTML. Default true.
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings The form block settings.
* @param array $context Request context.
*/
$send_html = (bool) apply_filters(
'generateblocks_form_email_use_html',
true,
$sanitized_data,
$form_settings,
$context
);
// Build headers. From email is static form config only; the display
// name may use sanitized merge tags.
$headers = [
$send_html
? 'Content-Type: text/html; charset=UTF-8'
: 'Content-Type: text/plain; charset=UTF-8',
];
$from_name = self::sanitize_from_name(
GenerateBlocks_Pro_Form_Sanitizer::replace_merge_tags(
$form_settings['emailFromName'] ?? '',
$sanitized_data
)
);
$from_email = self::sanitize_from_email( $form_settings['emailFromEmail'] ?? '' );
if ( '' !== $from_email ) {
$headers[] = self::build_from_header( $from_email, $from_name );
}
$reply_to = self::get_reply_to_email( $sanitized_data, $form_settings['emailReplyToField'] ?? '' );
if ( '' !== $reply_to ) {
$headers[] = 'Reply-To: ' . $reply_to;
}
$body = $send_html
? self::build_html_body( $sanitized_data, $context, $site_name )
: self::build_text_body( $sanitized_data, $context, $site_name );
/**
* Filter the email arguments before sending.
*
* @since 2.6.0
* @param array $email_args {
* Email arguments.
*
* @type array $to Recipient email addresses.
* @type string $subject Email subject line.
* @type string $body Email body content.
* @type array $headers Email headers.
* }
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings The form block settings.
* @param array $context Request context.
*/
$email_args = apply_filters(
'generateblocks_form_email_args',
[
'to' => $to,
'subject' => $subject,
'body' => $body,
'headers' => $headers,
],
$sanitized_data,
$form_settings,
$context
);
// Re-validate filter output: defensive against third-party callbacks
// returning values with embedded newlines (header injection) or
// reshaping the array entirely.
if ( ! is_array( $email_args ) ) {
$email_args = [];
}
$safe_to = self::sanitize_filter_recipients( $email_args['to'] ?? [] );
if ( empty( $safe_to ) ) {
return new WP_Error( 'email_no_recipients', 'No valid email recipient.' );
}
$raw_subject = $email_args['subject'] ?? '';
$safe_subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
is_string( $raw_subject ) ? $raw_subject : ''
);
$safe_headers = self::sanitize_filter_headers( $email_args['headers'] ?? [] );
$raw_body = $email_args['body'] ?? '';
$safe_body = is_string( $raw_body ) ? $raw_body : '';
$sent = self::send_mail(
$safe_to,
$safe_subject,
$safe_body,
$safe_headers,
'' === $from_email ? $from_name : ''
);
if ( ! $sent ) {
return new WP_Error( 'email_failed', 'Failed to send email.' );
}
return true;
}
/**
* Re-validate `to` returned by a filter callback. Accepts either a
* comma-separated string or an array of addresses.
*
* @param mixed $value The filtered `to` value.
* @return array Valid email addresses.
*/
private static function sanitize_filter_recipients( $value ) {
if ( is_string( $value ) ) {
return self::parse_recipients( $value );
}
if ( ! is_array( $value ) ) {
return [];
}
$valid = [];
foreach ( $value as $address ) {
if ( ! is_string( $address ) ) {
continue;
}
$address = sanitize_email( $address );
if ( is_email( $address ) ) {
$valid[] = $address;
if ( count( $valid ) >= self::MAX_RECIPIENTS ) {
break;
}
}
}
return $valid;
}
/**
* Resolve the configured Reply-To address.
*
* Empty config means auto-detect the first submitted email field. The
* explicit `__none` sentinel keeps an opt-out available without making the
* default contact-form path reply to the site sender.
*
* @param array $sanitized_data Submitted fields after schema-based sanitization.
* @param mixed $raw_field Configured reply-to field name.
* @return string Safe Reply-To email address, or empty string.
*/
private static function get_reply_to_email( $sanitized_data, $raw_field ) {
$reply_to_field = is_scalar( $raw_field ) ? sanitize_key( (string) $raw_field ) : '';
if ( self::REPLY_TO_NONE === $reply_to_field ) {
return '';
}
if ( '' !== $reply_to_field ) {
return self::get_reply_to_email_from_field( $sanitized_data, $reply_to_field );
}
foreach ( $sanitized_data as $field ) {
if ( ! is_array( $field ) || 'email' !== ( $field['type'] ?? '' ) ) {
continue;
}
return self::sanitize_reply_to_email( $field['value'] ?? '' );
}
return '';
}
/**
* Resolve Reply-To from a specific submitted field.
*
* @param array $sanitized_data Submitted fields after schema-based sanitization.
* @param string $field_name Sanitized field name.
* @return string Safe Reply-To email address, or empty string.
*/
private static function get_reply_to_email_from_field( $sanitized_data, $field_name ) {
$field = $sanitized_data[ $field_name ] ?? null;
if ( ! is_array( $field ) || 'email' !== ( $field['type'] ?? '' ) ) {
return '';
}
return self::sanitize_reply_to_email( $field['value'] ?? '' );
}
/**
* Sanitize and validate a Reply-To email address for header output.
*
* @param mixed $value Raw email value.
* @return string Safe Reply-To email address, or empty string.
*/
private static function sanitize_reply_to_email( $value ) {
$email = sanitize_email( is_scalar( $value ) ? (string) $value : '' );
if ( ! is_email( $email ) ) {
return '';
}
return GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $email );
}
/**
* Re-validate the `headers` array returned by a filter callback.
* Drops anything that's not a string and strips header-injection chars.
*
* Note: we only strip CR/LF/tab and their percent-encoded variants here.
* Running sanitize_text_field() would mangle valid display-name email
* syntax like "Reply-To: Name <a@b.com>" (strip_tags eats the <...>).
*
* @param mixed $headers The filtered headers value.
* @return array Sanitized headers.
*/
private static function sanitize_filter_headers( $headers ) {
if ( ! is_array( $headers ) ) {
return [];
}
$safe = [];
foreach ( $headers as $name => $header ) {
if ( is_string( $name ) && '' !== $name ) {
$header = $name . ': ' . ( is_scalar( $header ) ? (string) $header : '' );
}
if ( ! is_string( $header ) ) {
continue;
}
$header = preg_replace( '/[\r\n\t]/', '', $header );
$header = str_replace( [ '%0a', '%0d', '%0A', '%0D' ], '', $header );
$header = trim( $header );
if ( '' !== $header ) {
$safe[] = $header;
}
}
return $safe;
}
/**
* Send the email while forcing the content type parsed from our headers.
*
* WordPress applies the global wp_mail_content_type filter after parsing
* the headers passed to wp_mail(). Keep this message's MIME type scoped to
* the final sanitized headers so site-level filters cannot make HTML render
* as plain text or plain text render as HTML.
*
* @param array $to Recipient email addresses.
* @param string $subject Email subject.
* @param string $body Email body.
* @param array $headers Email headers.
* @param string $from_name Optional scoped From display name.
* @return bool Whether the email was sent.
*/
private static function send_mail( $to, $subject, $body, $headers, $from_name = '' ) {
$content_type = self::get_content_type_from_headers( $headers );
$content_type_filter = null;
$from_name = self::sanitize_from_name( $from_name );
$from_name_filter = null;
if ( '' !== $content_type ) {
$content_type_filter = static function () use ( $content_type ) {
return $content_type;
};
add_filter( 'wp_mail_content_type', $content_type_filter, 999 );
}
if ( '' !== $from_name && ! self::headers_include_from( $headers ) ) {
$from_name_filter = static function () use ( $from_name ) {
return $from_name;
};
add_filter( 'wp_mail_from_name', $from_name_filter, 999 );
}
try {
return wp_mail( $to, $subject, $body, $headers );
} finally {
if ( is_callable( $content_type_filter ) ) {
remove_filter( 'wp_mail_content_type', $content_type_filter, 999 );
}
if ( is_callable( $from_name_filter ) ) {
remove_filter( 'wp_mail_from_name', $from_name_filter, 999 );
}
}
}
/**
* Extract a supported MIME content type from headers.
*
* @param array $headers Email headers.
* @return string Supported content type, or an empty string.
*/
private static function get_content_type_from_headers( $headers ) {
$content_type = '';
foreach ( $headers as $header ) {
if ( ! is_string( $header ) || 0 !== stripos( $header, 'Content-Type:' ) ) {
continue;
}
$value = trim( substr( $header, strlen( 'Content-Type:' ) ) );
$type = strtolower( trim( explode( ';', $value, 2 )[0] ) );
if ( in_array( $type, [ 'text/html', 'text/plain' ], true ) ) {
$content_type = $type;
} elseif ( false !== strpos( $value, ';' ) || '' !== $type ) {
$content_type = '';
}
}
return $content_type;
}
/**
* Determine whether sanitized headers already include a From header.
*
* @param array $headers Email headers.
* @return bool
*/
private static function headers_include_from( $headers ) {
foreach ( $headers as $header ) {
if ( is_string( $header ) && 0 === stripos( trim( $header ), 'From:' ) ) {
return true;
}
}
return false;
}
/**
* Sanitize an admin-configured From email address.
*
* @param mixed $raw_email Raw From email setting.
* @return string Valid email address, or empty string.
*/
private static function sanitize_from_email( $raw_email ) {
$email = is_scalar( $raw_email ) ? sanitize_email( (string) $raw_email ) : '';
if ( ! is_email( $email ) ) {
return '';
}
return GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $email );
}
/**
* Sanitize a From display name.
*
* @param mixed $raw_name Raw From name setting.
* @return string Safe display name, or empty string.
*/
private static function sanitize_from_name( $raw_name ) {
$name = is_scalar( $raw_name ) ? sanitize_text_field( (string) $raw_name ) : '';
$name = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $name );
return trim( str_replace( [ '<', '>', '"', ',', ':', '\\' ], '', $name ) );
}
/**
* Build a safe From header from static form settings.
*
* @param string $email Valid From email address.
* @param string $name Optional display name.
* @return string Header line.
*/
private static function build_from_header( $email, $name ) {
if ( '' === $email ) {
return '';
}
if ( '' === $name ) {
return 'From: ' . $email;
}
return 'From: ' . $name . ' <' . $email . '>';
}
/**
* Parse and validate a comma-separated list of email recipients.
*
* @param string $raw Comma-separated email addresses.
* @return array Valid email addresses, or empty array if none valid.
*/
public static function parse_recipients( $raw ) {
$addresses = array_map( 'trim', explode( ',', $raw ) );
$valid = [];
foreach ( $addresses as $address ) {
$address = sanitize_email( $address );
if ( is_email( $address ) ) {
$valid[] = $address;
if ( count( $valid ) >= self::MAX_RECIPIENTS ) {
break;
}
}
}
return $valid;
}
/**
* Build the email body as HTML.
*
* @param array $sanitized_data The sanitized form data.
* @param array $context Request context.
* @param string $site_name The site name.
* @return string The formatted email body.
*/
private static function build_html_body( $sanitized_data, $context, $site_name ) {
$fields = [];
$rows = '';
foreach ( $sanitized_data as $field_name => $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$label = sanitize_text_field( $field['label'] ?? $field_name );
$value = wp_strip_all_tags( (string) ( $field['value'] ?? '' ), false );
if ( '' === $value ) {
continue;
}
$fields[] = [
'label' => $label,
'value' => $value,
'type' => (string) ( $field['type'] ?? 'text' ),
];
}
$field_count = count( $fields );
foreach ( $fields as $index => $field ) {
$rows .= self::build_html_field_row(
$field['label'],
$field['value'],
$field['type'],
$index === $field_count - 1
);
}
if ( '' === $rows ) {
$rows = '<tr><td style="padding:20px 0;color:#666666;font-family:Arial,sans-serif;font-size:14px;line-height:1.6;">'
. esc_html__( 'No field values were submitted.', 'generateblocks-pro' )
. '</td></tr>';
}
$meta_rows = self::build_html_meta_rows( $context, $site_name );
return '<table role="presentation" width="100%" cellpadding="24" cellspacing="0" border="0" bgcolor="#f5f5f5">'
. '<tr><td align="center">'
. '<table role="presentation" width="640" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff">'
. '<tr><td style="padding:10px 36px 24px 36px;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">'
. $rows
. '</table>'
. '</td></tr>'
. ( '' !== $meta_rows
? '<tr><td bgcolor="#fafafa" style="border-top:1px solid #eeeeee;padding:18px 36px;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">'
. $meta_rows
. '</table>'
. '</td></tr>'
: '' )
. '</table>'
. '</td></tr>'
. '</table>';
}
/**
* Build one field row for the HTML email body.
*
* @param string $label Field label.
* @param string $value Field value.
* @param string $type Field type.
* @param bool $last Whether this is the last field row.
* @return string Field row HTML.
*/
private static function build_html_field_row( $label, $value, $type, $last ) {
$border = $last ? '' : 'border-bottom:1px solid #eeeeee;';
return '<tr><td style="padding:16px 0 4px 0;color:#767676;font-family:Arial,sans-serif;font-size:12px;">'
. esc_html( self::format_label( $label ) )
. '</td></tr>'
. '<tr><td style="' . $border . 'color:#1f1f1f;font-family:Arial,sans-serif;font-size:15px;line-height:1.6;padding:0 0 16px 0;">'
. self::format_html_value( $value, $type )
. '</td></tr>';
}
/**
* Format a submitted value for HTML email output.
*
* @param string $value Field value.
* @param string $type Field type.
* @return string HTML-safe value.
*/
private static function format_html_value( $value, $type ) {
$value = str_replace( [ "\r\n", "\r" ], "\n", $value );
if ( 'email' === $type ) {
$email = sanitize_email( $value );
if ( is_email( $email ) ) {
return '<a href="' . esc_url( 'mailto:' . $email ) . '" style="color:#1a5fb4;">'
. esc_html( $value )
. '</a>';
}
}
if ( 'url' === $type ) {
$url = esc_url( $value );
if ( '' !== $url ) {
return '<a href="' . $url . '" style="color:#1a5fb4;">'
. esc_html( $value )
. '</a>';
}
}
return nl2br( esc_html( $value ), false );
}
/**
* Build metadata rows for the HTML email footer.
*
* @param array $context Request context.
* @param string $site_name Site name.
* @return string Metadata rows HTML.
*/
private static function build_html_meta_rows( $context, $site_name ) {
$rows = '';
if ( '' !== $site_name ) {
$rows .= self::build_html_meta_row( __( 'Site', 'generateblocks-pro' ), esc_html( $site_name ) );
}
$form_title = self::get_form_title( $context );
if ( '' !== $form_title ) {
$rows .= self::build_html_meta_row( __( 'Form', 'generateblocks-pro' ), esc_html( $form_title ) );
}
if ( ! empty( $context['page_url'] ) ) {
$url = esc_url( $context['page_url'] );
if ( '' !== $url ) {
$rows .= self::build_html_meta_row(
__( 'Page', 'generateblocks-pro' ),
'<a href="' . $url . '" style="color:#1a5fb4;">' . esc_html( $context['page_url'] ) . '</a>'
);
}
}
$rows .= self::build_html_meta_row(
__( 'Submitted', 'generateblocks-pro' ),
esc_html( wp_date( get_option( 'date_format' ) . ' @ ' . get_option( 'time_format' ) ) )
);
return $rows;
}
/**
* Build one metadata row for the HTML email footer.
*
* @param string $label Metadata label.
* @param string $value Metadata value HTML.
* @return string Metadata row HTML.
*/
private static function build_html_meta_row( $label, $value ) {
return '<tr>'
. '<td valign="top" style="color:#767676;font-family:Arial,sans-serif;font-size:12px;line-height:1.5;padding:3px 14px 3px 0;">'
. esc_html( self::format_label( $label ) )
. '</td>'
. '<td valign="top" style="color:#444444;font-family:Arial,sans-serif;font-size:13px;line-height:1.5;padding:3px 0;">'
. $value
. '</td>'
. '</tr>';
}
/**
* Format labels without depending on CSS text-transform support.
*
* @param string $label Label text.
* @return string Uppercase label.
*/
private static function format_label( $label ) {
return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $label ) : strtoupper( $label );
}
/**
* Get the form title from the request context.
*
* @param array $context Request context.
* @return string Form title.
*/
private static function get_form_title( $context ) {
$form_id = isset( $context['form_id'] ) ? (int) $context['form_id'] : 0;
if ( $form_id <= 0 ) {
return '';
}
return sanitize_text_field( get_the_title( $form_id ) );
}
/**
* Build the email body as plain text.
*
* Uses uppercase labels on their own line for a clear scan pattern.
* Long values and multi-line content (textareas) flow naturally.
* The email client handles all rendering — fonts, colors, dark mode.
*
* @param array $sanitized_data The sanitized form data.
* @param array $context Request context.
* @param string $site_name The site name.
* @return string The formatted email body.
*/
private static function build_text_body( $sanitized_data, $context, $site_name ) {
$lines = [];
foreach ( $sanitized_data as $field_name => $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$label = sanitize_text_field( $field['label'] ?? $field_name );
$value = wp_strip_all_tags( (string) ( $field['value'] ?? '' ) );
if ( '' === $value ) {
continue;
}
$lines[] = function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $label ) : strtoupper( $label );
$lines[] = $value;
$lines[] = '';
}
// Divider before metadata.
$lines[] = str_repeat( "\xe2\x94\x80", 40 );
// Metadata on compact lines.
$meta = [ $site_name ];
if ( ! empty( $context['page_url'] ) ) {
$meta[] = $context['page_url'];
}
$meta[] = wp_date( get_option( 'date_format' ) . ' @ ' . get_option( 'time_format' ) );
$lines[] = implode( " \xc2\xb7 ", $meta );
return implode( "\n", $lines );
}
}
@@ -0,0 +1,67 @@
<?php
/**
* Form action registry.
*
* Extensible system for registering form submission handlers.
* Third-party developers can register their own actions.
* All callbacks receive sanitized data only.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form action registry class.
*/
class GenerateBlocks_Pro_Form_Action_Registry {
/**
* Registered actions.
*
* @var array<string, callable>
*/
private static $actions = [];
/**
* Register a form action.
*
* @param string $name The action identifier (e.g., 'email', 'mailchimp').
* @param callable $callback The handler function. Signature: function( array $sanitized_data, array $form_settings ): WP_Error|true.
*/
public static function register( $name, $callback ) {
if ( is_callable( $callback ) ) {
self::$actions[ sanitize_key( $name ) ] = $callback;
}
}
/**
* Get a registered action callback.
*
* @param string $name The action identifier.
* @return callable|null The callback or null if not registered.
*/
public static function get( $name ) {
return self::$actions[ sanitize_key( $name ) ] ?? null;
}
/**
* Unregister a form action.
*
* @param string $name The action identifier.
*/
public static function unregister( $name ) {
unset( self::$actions[ sanitize_key( $name ) ] );
}
/**
* Get all registered action names.
*
* @return array List of registered action identifiers.
*/
public static function get_registered() {
return array_keys( self::$actions );
}
}
@@ -0,0 +1,221 @@
<?php
/**
* Webhook form action.
*
* POSTs form submission data as JSON to a configured URL.
* Uses wp_safe_remote_post() to block requests to internal/private IPs.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Webhook form action class.
*/
class GenerateBlocks_Pro_Form_Action_Webhook {
/**
* Register the webhook action with the registry.
*/
public static function init() {
GenerateBlocks_Pro_Form_Action_Registry::register( 'webhook', [ __CLASS__, 'send' ] );
}
/**
* Validate the webhook action settings before delivery starts.
*
* @param array $form_settings The form block settings.
* @return true|WP_Error
*/
public static function validate_settings( $form_settings ) {
$url = self::get_valid_webhook_url( $form_settings );
return is_wp_error( $url ) ? $url : true;
}
/**
* Send the form submission to the configured webhook URL.
*
* @param array $sanitized_data The sanitized form data (field_name => ['value' => ..., 'label' => ..., 'type' => ...]).
* @param array $form_settings The form block settings.
* @param array $context Request context (post_id, page_url).
* @return true|WP_Error True on success, WP_Error on failure.
*/
public static function send( $sanitized_data, $form_settings, $context = [] ) {
$webhook_url = self::get_valid_webhook_url( $form_settings );
if ( is_wp_error( $webhook_url ) ) {
return $webhook_url;
}
return self::send_to_url( $webhook_url, $sanitized_data, $form_settings, $context );
}
/**
* Validate email-signup webhook provider settings.
*
* @param array $settings Email signup settings.
* @return true|WP_Error
*/
public static function validate_signup_settings( $settings ) {
$url = self::get_valid_url( $settings['url'] ?? '' );
return is_wp_error( $url ) ? $url : true;
}
/**
* Send an email signup submission to a webhook provider URL.
*
* @param array $sanitized_data The sanitized form data.
* @param array $settings Email signup settings.
* @param array $context Request context.
* @return true|WP_Error
*/
public static function subscribe( $sanitized_data, $settings, $context = [] ) {
$webhook_url = self::get_valid_url( $settings['url'] ?? '' );
if ( is_wp_error( $webhook_url ) ) {
return $webhook_url;
}
$form_settings = [
'actionSettings' => [
'email-signup' => $settings,
'webhook' => [
'url' => $settings['url'] ?? '',
],
],
];
return self::send_to_url( $webhook_url, $sanitized_data, $form_settings, $context );
}
/**
* Send sanitized form data to a validated webhook URL.
*
* @param string $webhook_url Validated webhook URL.
* @param array $sanitized_data The sanitized form data.
* @param array $form_settings Form settings passed to filters.
* @param array $context Request context.
* @return true|WP_Error
*/
private static function send_to_url( $webhook_url, $sanitized_data, $form_settings, $context = [] ) {
// Build a clean payload — field name => value (flat), plus metadata.
$fields = [];
foreach ( $sanitized_data as $field_name => $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$fields[ $field_name ] = (string) ( $field['value'] ?? '' );
}
/**
* Filter the webhook payload before sending.
*
* Field values are sent flat at the top level — receivers like
* FluentCRM only map top-level keys (e.g. `email` is required there) —
* and repeated under `fields` for consumers that map structured paths.
* On a key collision a field value wins the top-level slot.
*
* @since 2.6.0
* @param array $payload The webhook payload.
* @param array $sanitized_data The full sanitized form data with labels and types.
* @param array $form_settings The form block settings.
* @param array $context Request context.
*/
$payload = apply_filters(
'generateblocks_form_webhook_payload',
array_merge(
[
'form_name' => ! empty( $context['form_id'] ) ? get_the_title( $context['form_id'] ) : '',
'fields' => $fields,
'page_url' => $context['page_url'] ?? '',
'timestamp' => gmdate( 'c' ),
],
$fields
),
$sanitized_data,
$form_settings,
$context
);
// wp_safe_remote_post blocks requests to internal/private IPs (127.0.0.1,
// 10.x.x.x, 169.254.169.254, etc.) via wp_http_validate_url(). This prevents
// SSRF since the webhook URL is user-supplied. Redirection is disabled so a
// 302 to an internal IP can't bypass the validation.
$response = wp_safe_remote_post(
$webhook_url,
[
'body' => wp_json_encode( $payload ),
'headers' => [ 'Content-Type' => 'application/json' ],
'timeout' => GenerateBlocks_Pro_Form_Http::DEFAULT_TIMEOUT,
'redirection' => 0,
'data_format' => 'body',
]
);
if ( is_wp_error( $response ) ) {
return new WP_Error(
'webhook_failed',
'Webhook request failed: ' . $response->get_error_message()
);
}
$status_code = wp_remote_retrieve_response_code( $response );
// Accept any 2xx response.
if ( $status_code < 200 || $status_code >= 300 ) {
return new WP_Error(
'webhook_failed',
'Webhook returned status ' . $status_code . '.'
);
}
return true;
}
/**
* Get the validated webhook URL.
*
* @param array $form_settings The form block settings.
* @return string|WP_Error
*/
private static function get_valid_webhook_url( $form_settings ) {
$webhook_url = $form_settings['actionSettings']['webhook']['url'] ?? '';
return self::get_valid_url( $webhook_url );
}
/**
* Validate a webhook URL.
*
* @param mixed $webhook_url Raw webhook URL.
* @return string|WP_Error
*/
private static function get_valid_url( $webhook_url ) {
if ( empty( $webhook_url ) ) {
return new WP_Error(
'missing_webhook_url',
'Webhook URL is not configured.',
[ 'status' => 400 ]
);
}
$webhook_url = esc_url_raw( $webhook_url, [ 'https', 'http' ] );
if ( empty( $webhook_url ) || ! wp_http_validate_url( $webhook_url ) ) {
return new WP_Error(
'invalid_webhook_url',
'Invalid webhook URL.',
[ 'status' => 400 ]
);
}
return $webhook_url;
}
}
@@ -0,0 +1,118 @@
<?php
/**
* Shared HTTP helpers for form integrations.
*
* Converts non-2xx responses from third-party APIs into actionable WP_Errors
* so auth failures, rate limits, and outages don't silently look like
* "no lists / no forms / no groups" in the editor.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form HTTP helper class.
*/
class GenerateBlocks_Pro_Form_Http {
/**
* Default timeout for form submission HTTP actions.
*
* Multiple actions can run for a single submission, so keep this lower
* than WordPress' default to avoid tying up PHP workers for too long.
*/
const DEFAULT_TIMEOUT = 8;
/**
* Convert a non-2xx HTTP response into a WP_Error with a useful message.
*
* Returns null when the response is a 2xx so callers can continue. Checks
* common JSON error-message keys used across providers.
*
* @param array $response The wp_remote_* response (not a WP_Error — caller checks first).
* @param string $service Human-readable service name for the error message.
* @return WP_Error|null
*/
public static function response_error( $response, $service ) {
$status = wp_remote_retrieve_response_code( $response );
if ( $status >= 200 && $status < 300 ) {
return null;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$detail = '';
if ( is_array( $body ) ) {
// Try each common error-message field.
foreach ( [ 'detail', 'message', 'error' ] as $key ) {
if ( ! empty( $body[ $key ] ) && is_string( $body[ $key ] ) ) {
$detail = $body[ $key ];
break;
}
}
}
$message = $service . ': ';
if ( 401 === $status || 403 === $status ) {
$message .= __( 'authentication failed — check your API key.', 'generateblocks-pro' );
} elseif ( 429 === $status ) {
$retry_after = wp_remote_retrieve_header( $response, 'retry-after' );
$message .= __( 'rate limit exceeded.', 'generateblocks-pro' );
if ( $retry_after ) {
$message .= ' ' . sprintf(
/* translators: %s: seconds */
__( 'Retry after %s seconds.', 'generateblocks-pro' ),
sanitize_text_field( (string) $retry_after )
);
}
} elseif ( $status >= 500 ) {
$message .= __( 'provider error — try again later.', 'generateblocks-pro' );
} elseif ( $detail ) {
$message .= sanitize_text_field( $detail );
} else {
$message .= sprintf(
/* translators: %d: HTTP status code */
__( 'unexpected response (HTTP %d).', 'generateblocks-pro' ),
$status
);
}
return new WP_Error( 'provider_error', $message, [ 'status' => $status ] );
}
/**
* Normalize a submitted field value for provider payloads.
*
* Form fields can produce scalar values or arrays (for example checkbox
* groups). Provider merge/custom-field APIs expect strings, so flatten arrays
* consistently and reject objects or nested non-scalar array values.
*
* @param mixed $value Submitted value.
* @return string
*/
public static function normalize_field_value( $value ) {
if ( is_array( $value ) ) {
$parts = [];
foreach ( $value as $part ) {
if ( is_scalar( $part ) ) {
$parts[] = (string) $part;
}
}
$value = implode( ', ', $parts );
}
if ( is_object( $value ) ) {
return '';
}
return trim( (string) $value );
}
}
@@ -0,0 +1,445 @@
<?php
/**
* Form integration provider registry.
*
* Stores provider definitions for fixed form integrations. Provider IDs are
* settings of a form action, not form actions themselves.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Integration registry class.
*/
class GenerateBlocks_Pro_Form_Integration_Registry {
/**
* Registered provider definitions.
*
* @var array<string, array>
*/
private static $integrations = [];
/**
* Registered API keys.
*
* @var array<string, string>
*/
private static $keys = [];
/**
* Register an integration provider.
*
* @param array $args Provider definition.
* @return bool True when registered.
*/
public static function register( $args ) {
if ( ! is_array( $args ) ) {
return false;
}
$id = isset( $args['id'] ) ? sanitize_key( $args['id'] ) : '';
if ( '' === $id || empty( $args['label'] ) ) {
return false;
}
self::$integrations[ $id ] = [
'id' => $id,
'label' => sanitize_text_field( (string) $args['label'] ),
'type' => isset( $args['type'] ) ? sanitize_key( $args['type'] ) : 'email',
'help' => isset( $args['help'] ) ? sanitize_text_field( (string) $args['help'] ) : '',
'connection' => self::normalize_connection( $args['connection'] ?? [] ),
'destination' => self::normalize_destination( $args['destination'] ?? [] ),
'secondary_destination' => self::normalize_destination( $args['secondary_destination'] ?? [], false ),
'settings_fields' => self::normalize_settings_fields( $args['settings_fields'] ?? [] ),
'settings_validate_callback' => isset( $args['settings_validate_callback'] ) && is_callable( $args['settings_validate_callback'] )
? $args['settings_validate_callback']
: null,
'field_map' => self::normalize_field_map( $args['field_map'] ?? [] ),
'supports_name_field' => ! empty( $args['supports_name_field'] ),
'supports_double_optin' => ! empty( $args['supports_double_optin'] ),
'default_double_optin' => ! array_key_exists( 'default_double_optin', $args ) || ! empty( $args['default_double_optin'] ),
'subscribe_callback' => isset( $args['subscribe_callback'] ) && is_callable( $args['subscribe_callback'] )
? $args['subscribe_callback']
: null,
];
if ( method_exists( 'GenerateBlocks_Pro_Form_Integration_Rest', 'clear_service_cache' ) ) {
GenerateBlocks_Pro_Form_Integration_Rest::clear_service_cache( $id );
}
return true;
}
/**
* Normalize a connection definition.
*
* @param array $connection Raw connection definition.
* @return array
*/
private static function normalize_connection( $connection ) {
$connection = is_array( $connection ) ? $connection : [];
$type = isset( $connection['type'] ) ? sanitize_key( $connection['type'] ) : 'none';
$fields = isset( $connection['fields'] ) && is_array( $connection['fields'] )
? $connection['fields']
: [];
if ( 'api_key' === $type && empty( $fields ) ) {
$fields = [
[
'key' => 'api_key',
'label' => __( 'API Key', 'generateblocks-pro' ),
'type' => 'password',
'required' => true,
],
];
}
return [
'type' => $type,
'fields' => self::normalize_connection_fields( $fields ),
'test_callback' => isset( $connection['test_callback'] ) && is_callable( $connection['test_callback'] )
? $connection['test_callback']
: null,
'connected_callback' => isset( $connection['connected_callback'] ) && is_callable( $connection['connected_callback'] )
? $connection['connected_callback']
: null,
];
}
/**
* Normalize connection fields.
*
* @param array $fields Raw fields.
* @return array
*/
private static function normalize_connection_fields( $fields ) {
$out = [];
foreach ( $fields as $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$key = isset( $field['key'] ) ? preg_replace( '/[^A-Za-z0-9_]/', '', (string) $field['key'] ) : '';
if ( '' === $key || empty( $field['label'] ) ) {
continue;
}
$out[] = [
'key' => $key,
'label' => sanitize_text_field( (string) $field['label'] ),
'type' => isset( $field['type'] ) ? sanitize_key( $field['type'] ) : 'text',
'help' => isset( $field['help'] ) ? sanitize_text_field( (string) $field['help'] ) : '',
'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '',
'required' => ! array_key_exists( 'required', $field ) || ! empty( $field['required'] ),
'test_required' => array_key_exists( 'test_required', $field )
? ! empty( $field['test_required'] )
: ( ! array_key_exists( 'required', $field ) || ! empty( $field['required'] ) ),
'define' => isset( $field['define'] ) ? preg_replace( '/[^A-Z0-9_]/', '', (string) $field['define'] ) : '',
'public' => ! empty( $field['public'] ),
];
}
return $out;
}
/**
* Normalize the provider destination definition.
*
* @param array $destination Raw definition.
* @param bool $default_required Whether the destination is required.
* @return array
*/
private static function normalize_destination( $destination, $default_required = true ) {
$destination = is_array( $destination ) ? $destination : [];
$has_destination = ! empty( $destination['label'] ) || isset( $destination['options_callback'] );
return [
'label' => isset( $destination['label'] ) ? sanitize_text_field( (string) $destination['label'] ) : '',
'empty_label' => isset( $destination['empty_label'] ) ? sanitize_text_field( (string) $destination['empty_label'] ) : __( '- Select -', 'generateblocks-pro' ),
'refresh_label' => isset( $destination['refresh_label'] ) ? sanitize_text_field( (string) $destination['refresh_label'] ) : __( 'Refresh', 'generateblocks-pro' ),
'required' => array_key_exists( 'required', $destination ) ? ! empty( $destination['required'] ) : ( $default_required && $has_destination ),
'options_callback' => isset( $destination['options_callback'] ) && is_callable( $destination['options_callback'] )
? $destination['options_callback']
: null,
];
}
/**
* Normalize provider-specific form settings.
*
* @param array $fields Raw fields.
* @return array
*/
private static function normalize_settings_fields( $fields ) {
if ( ! is_array( $fields ) ) {
return [];
}
$out = [];
foreach ( $fields as $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$key = isset( $field['key'] ) ? preg_replace( '/[^A-Za-z0-9_]/', '', (string) $field['key'] ) : '';
if ( '' === $key || empty( $field['label'] ) ) {
continue;
}
$out[] = [
'key' => $key,
'label' => sanitize_text_field( (string) $field['label'] ),
'type' => isset( $field['type'] ) ? sanitize_key( $field['type'] ) : 'text',
'help' => isset( $field['help'] ) ? sanitize_text_field( (string) $field['help'] ) : '',
'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( (string) $field['placeholder'] ) : '',
'required' => ! array_key_exists( 'required', $field ) || ! empty( $field['required'] ),
];
}
return $out;
}
/**
* Normalize field mapping support.
*
* @param array $field_map Raw definition.
* @return array
*/
private static function normalize_field_map( $field_map ) {
$field_map = is_array( $field_map ) ? $field_map : [];
$callback = isset( $field_map['options_callback'] ) && is_callable( $field_map['options_callback'] )
? $field_map['options_callback']
: null;
return [
'enabled' => ! empty( $field_map['enabled'] ) || is_callable( $callback ),
'label' => isset( $field_map['label'] ) ? sanitize_text_field( (string) $field_map['label'] ) : __( 'Fields', 'generateblocks-pro' ),
'empty_label' => isset( $field_map['empty_label'] ) ? sanitize_text_field( (string) $field_map['empty_label'] ) : __( 'Don\'t map', 'generateblocks-pro' ),
'refresh_label' => isset( $field_map['refresh_label'] ) ? sanitize_text_field( (string) $field_map['refresh_label'] ) : __( 'Refresh fields', 'generateblocks-pro' ),
'depends_on_destination' => ! empty( $field_map['depends_on_destination'] ),
'options_callback' => $callback,
];
}
/**
* Normalize options into { label, value } rows.
*
* @param mixed $options Raw options.
* @return array
*/
public static function normalize_options( $options ) {
if ( ! is_array( $options ) ) {
return [];
}
$out = [];
foreach ( $options as $option ) {
if ( is_array( $option ) ) {
$value = $option['value'] ?? ( $option['id'] ?? ( $option['tag'] ?? '' ) );
$label = $option['label'] ?? ( $option['name'] ?? ( $option['tag'] ?? $value ) );
$count = $option['count'] ?? null;
} else {
$value = $option;
$label = $option;
$count = null;
}
if ( is_array( $value ) || is_object( $value ) || is_array( $label ) || is_object( $label ) ) {
continue;
}
$row = [
'value' => sanitize_text_field( (string) $value ),
'label' => sanitize_text_field( (string) $label ),
];
if ( null !== $count && is_numeric( $count ) ) {
$row['count'] = (int) $count;
}
$out[] = $row;
}
return $out;
}
/**
* Get a registered provider definition.
*
* @param string $id Provider ID.
* @return array|null
*/
public static function get_provider( $id ) {
return self::$integrations[ sanitize_key( $id ) ] ?? null;
}
/**
* Backward-compatible internal alias for provider lookups.
*
* @param string $id Provider ID.
* @return array|null
*/
public static function get_integration( $id ) {
return self::get_provider( $id );
}
/**
* Get all registered provider definitions.
*
* @return array
*/
public static function get_all() {
return self::$integrations;
}
/**
* Get public provider definitions for the editor.
*
* Callback references, define names, and other server-only data are omitted.
*
* @return array
*/
public static function get_public_definitions() {
return array_values(
array_map(
[ __CLASS__, 'to_public_definition' ],
self::$integrations
)
);
}
/**
* Convert a provider definition into the public editor shape.
*
* @param array $integration Provider definition.
* @return array
*/
private static function to_public_definition( $integration ) {
return [
'id' => $integration['id'],
'label' => $integration['label'],
'type' => $integration['type'],
'help' => $integration['help'],
'connection' => [
'type' => $integration['connection']['type'],
'fields' => array_map(
function ( $field ) {
return [
'key' => $field['key'],
'label' => $field['label'],
'type' => $field['type'],
'help' => $field['help'],
'placeholder' => $field['placeholder'],
'required' => $field['required'],
'testRequired' => $field['test_required'],
];
},
$integration['connection']['fields']
),
],
'destination' => [
'label' => $integration['destination']['label'],
'emptyLabel' => $integration['destination']['empty_label'],
'refreshLabel' => $integration['destination']['refresh_label'],
'required' => $integration['destination']['required'],
'hasOptions' => is_callable( $integration['destination']['options_callback'] ),
],
'secondaryDestination' => [
'label' => $integration['secondary_destination']['label'],
'emptyLabel' => $integration['secondary_destination']['empty_label'],
'refreshLabel' => $integration['secondary_destination']['refresh_label'],
'required' => $integration['secondary_destination']['required'],
'hasOptions' => is_callable( $integration['secondary_destination']['options_callback'] ),
],
'settingsFields' => array_map(
function ( $field ) {
return [
'key' => $field['key'],
'label' => $field['label'],
'type' => $field['type'],
'help' => $field['help'],
'placeholder' => $field['placeholder'],
'required' => $field['required'],
];
},
$integration['settings_fields']
),
'fieldMap' => [
'enabled' => $integration['field_map']['enabled'],
'label' => $integration['field_map']['label'],
'emptyLabel' => $integration['field_map']['empty_label'],
'refreshLabel' => $integration['field_map']['refresh_label'],
'dependsOnDestination' => $integration['field_map']['depends_on_destination'],
'hasOptions' => is_callable( $integration['field_map']['options_callback'] ),
],
'supportsNameField' => $integration['supports_name_field'],
'supportsDoubleOptin' => $integration['supports_double_optin'],
'defaultDoubleOptin' => $integration['default_double_optin'],
'canSubscribe' => is_callable( $integration['subscribe_callback'] ),
];
}
/**
* Register an API key for a service.
*
* @param string $service The service identifier.
* @param string $api_key The API key.
*/
public static function set( $service, $api_key ) {
$service = sanitize_key( $service );
self::$keys[ $service ] = $api_key;
if ( method_exists( 'GenerateBlocks_Pro_Form_Integration_Rest', 'clear_service_cache' ) ) {
GenerateBlocks_Pro_Form_Integration_Rest::clear_service_cache( $service );
}
}
/**
* Get a registered API key.
*
* @param string $service The service identifier.
* @return string The API key, or empty string if not registered.
*/
public static function get( $service ) {
return self::$keys[ sanitize_key( $service ) ] ?? '';
}
}
/**
* Register an email integration API key.
*
* Call this inside the 'generateblocks_form_register_email_integrations' hook:
*
* add_action( 'generateblocks_form_register_email_integrations', function() {
* generateblocks_pro_set_email_integration( 'mailchimp', 'your-api-key' );
* } );
*
* @param string $service The service identifier.
* @param string $api_key The API key.
*/
function generateblocks_pro_set_email_integration( $service, $api_key ) {
GenerateBlocks_Pro_Form_Integration_Registry::set( $service, $api_key );
}
/**
* Register a form integration provider.
*
* @param array $args Provider definition.
* @return bool True when registered.
*/
function generateblocks_pro_register_form_integration( $args ) {
return GenerateBlocks_Pro_Form_Integration_Registry::register( $args );
}

Some files were not shown because too many files have changed in this diff Show More