$item) { if ( isset($item['control']) && self::getControlValue($item['control'], 1) === true // Is this control enabled? @TODO Dynamic way to define default value depending the control ; not all will be active (1) by default && self::isBlockEnabled($block['blockName']) // Controls are enabled for this block? && isset($item['enabled']) && (bool) $item['enabled'] === true ) { // skip some control if (in_array($item['control'], ['meta'])) { continue; } // apply preset control if ($item['control'] === 'presets') { // Presets return the content passed in when they match if (!self::checkPresetControls('1', $block, $item, $key)) { $show_block = false; } } elseif (self::displayBlock($block, $item['control'], $key) === false) { // We reached a control that decides block shouldn't be displayed $show_block = false; } } } return $show_block; } /** * Excerpts built from the post content (no manual excerpt) render the * inner blocks of wrapper blocks such as core/group and core/columns * without running render_block on the wrapper. While WordPress builds * such an excerpt, remove the blocks hidden by their controls from the * content first, so the excerpt matches what the post shows. * * Hooked to get_the_excerpt before wp_trim_excerpt (priority 10). * * @param string $excerpt Manual excerpt, empty when it will be generated * * @return string * @since 3.7.7 */ public static function startExcerptFiltering($excerpt) { if (trim((string) $excerpt) === '' && Utilities::settingIsEnabled('block_controls')) { self::$excerpt_depth++; add_filter('content_pagination', [__CLASS__, 'removeHiddenBlocksFromPages'], 10, 1); } return $excerpt; } /** * Stop filtering the content once the excerpt is built (after wp_trim_excerpt) * * @param string $excerpt Excerpt * * @return string * @since 3.7.7 */ public static function endExcerptFiltering($excerpt) { if (self::$excerpt_depth > 0) { self::$excerpt_depth--; if (self::$excerpt_depth === 0) { remove_filter('content_pagination', [__CLASS__, 'removeHiddenBlocksFromPages'], 10); } } return $excerpt; } /** * @var int Nesting level of excerpts being generated */ private static $excerpt_depth = 0; /** * Remove hidden blocks from the post pages used to build the excerpt * * @param array $pages Post content split by * * @return array * @since 3.7.7 */ public static function removeHiddenBlocksFromPages($pages) { if (! is_array($pages)) { return $pages; } foreach ($pages as $index => $page) { $pages[$index] = self::removeHiddenBlocks($page); } return $pages; } /** * Remove blocks hidden by their controls, with their inner blocks, from block markup * * @param string $content Block markup * * @return string Content without hidden blocks; unchanged when nothing is hidden * @since 3.7.7 */ public static function removeHiddenBlocks($content) { if ( ! is_string($content) || strpos($content, 'advgbBlockControls') === false || ! function_exists('parse_blocks') || ! has_blocks($content) ) { return $content; } $blocks = parse_blocks($content); $changed = false; $blocks = self::filterHiddenBlocks($blocks, $changed); return $changed ? serialize_blocks($blocks) : $content; } /** * Recursively drop hidden blocks from a list of parsed blocks * * @param array $blocks Parsed blocks * @param bool $changed Set to true when a block was removed * * @return array * @since 3.7.7 */ private static function filterHiddenBlocks($blocks, &$changed) { $result = []; foreach ($blocks as $block) { if (! self::isBlockVisible($block)) { $changed = true; continue; } if (! empty($block['innerBlocks'])) { $inner_changed = false; $kept = []; foreach ($block['innerBlocks'] as $inner_index => $inner_block) { $filtered = self::filterHiddenBlocks([$inner_block], $inner_changed); $kept[ $inner_index ] = $filtered ? $filtered[0] : null; } if ($inner_changed) { $changed = true; // innerContent holds a null placeholder for each inner block, in order $inner_content = []; $inner_index = 0; foreach ($block['innerContent'] as $chunk) { if ($chunk === null) { if (isset($kept[ $inner_index ])) { $inner_content[] = null; } $inner_index++; continue; } $inner_content[] = $chunk; } $block['innerContent'] = $inner_content; $block['innerBlocks'] = array_values(array_filter($kept)); } } $result[] = $block; } return $result; } private static function getPresetItemConfig($preset_id, $presets = false) { $presets = ! is_array($presets) ? self::getPresets(true) : $presets; $preset_config = false; if (isset($presets[$preset_id])) { $preset_config = $presets[$preset_id]; } return $preset_config; } private static function checkPresetControls($block_content, $block, $preset_config, $key) { $selected_presets = $preset_config['selected'] ?? []; $logic = $preset_config['logic'] ?? 'any'; $preset_matches = 0; foreach ($selected_presets as $preset_id) { $preset_rule = self::getPresetItemConfig($preset_id); if ($preset_rule) { if (self::evaluatePreset($preset_rule, $block, $key)) { $preset_matches++; } } } $show_block = false; if ($logic === 'any') { $show_block = $preset_matches > 0; } else { $show_block = $preset_matches === count($selected_presets); } return $show_block ? $block_content : ''; } private static function evaluatePreset($preset, $block, $key) { $control_sets = $preset['controlSets'] ?? []; // OR logic between control sets foreach ($control_sets as $control_set) { if (self::evaluateControlSet($control_set, $block, $key)) { return true; // If any control set matches, preset matches } } return false; } private static function evaluateControlSet($control_set, $block, $key) { $rules = $control_set['rules'] ?? []; // AND logic within control set foreach ($rules as $rule) { if (!self::evaluateRule($rule, $block, $key)) { return false; // If any rule fails, control set fails } } return true; // All rules passed } private static function evaluateRule($rule, $block, $key) { $rule_type = $rule['type'] ?? ''; // Replace preset id with data $block['attrs']['advgbBlockControls'][$key] = array_merge([ 'control' => $rule_type, 'enabled' => true ], $rule); return self::displayBlock($block, $rule_type, $key); } /** * Check if block in widgets area is using controls and decide to display or not in frontend, * including its widget HTML wrapper. * * @param array $instance Widget instance * * @return bool false means block and its widget HTML wrapper is hidden * @since 3.1.2 * */ public static function checkBlockControlsWidget($instance) { // Exclude REST API if ( strpos(wp_get_raw_referer(), '/wp-admin/widgets.php') && isset($_SERVER['REQUEST_URI']) && false !== strpos( filter_var(wp_unslash($_SERVER['REQUEST_URI']), FILTER_SANITIZE_URL), '/wp-json/' ) ) { return $instance; } if ( Utilities::settingIsEnabled('block_controls') && !empty($instance['content']) && has_blocks($instance['content']) ) { $blocks = parse_blocks($instance['content']); if (isset($blocks[0]['attrs']['advgbBlockControls']) && $blocks[0]['blockName']) { $controls = $blocks[0]['attrs']['advgbBlockControls']; foreach ($controls as $key => $item) { if ( isset($item['control']) && self::getControlValue( $item['control'], 1 ) === true // Is this control enabled? @TODO Dynamic way to define default value depending the control ; not all will be active (1) by default && self::isBlockEnabled($blocks[0]['blockName']) // Controls are enabled for this block? && isset($item['enabled']) && (bool) $item['enabled'] === true ) { if (self::displayBlock($blocks[0], $item['control'], $key) === false) { return false; // This block is hidden } } } } } return $instance; } /** * Check a single control against a block * * @param array $block Block object * @param string $control Control to validate against a block. e.g. 'schedule' * @param int $key Array position for $control * @param mixed $device_type The current user devce type * * @return bool True to display block, false to hide * @since 3.1.0 * */ private static function displayBlock($block, $control, $key, $device_type = null) { switch ($control) { // Schedule control default: case 'schedule': $bControl = $block['attrs']['advgbBlockControls'][$key]; // Backward compatibility - check if we have the old single schedule format if (isset($bControl['schedules'])) { // New format with multiple schedules $schedules = $bControl['schedules']; } else { // Legacy format - convert to array $schedules = [[ 'dateFrom' => $bControl['dateFrom'] ?? null, 'dateTo' => $bControl['dateTo'] ?? null, 'recurring' => $bControl['recurring'] ?? false, 'days' => $bControl['days'] ?? [], 'timeFrom' => $bControl['timeFrom'] ?? null, 'timeTo' => $bControl['timeTo'] ?? null, 'timezone' => $bControl['timezone'] ?? null ]]; } $shouldShow = false; foreach ($schedules as $schedule) { // Skip if schedule is empty if ( empty($schedule['dateFrom']) && empty($schedule['dateTo']) && empty($schedule['timeFrom']) && empty($schedule['timeTo']) && empty($schedule['days']) ) { continue; } $dateFrom = $dateTo = $recurring = $timeFrom = $timeTo = null; $days = isset($schedule['days']) && is_array($schedule['days']) && count($schedule['days']) ? $schedule['days'] : []; if (count($days)) { // Convert JavaScript days (0=Sun) to PHP 'N' format (7=Sun) $days = array_map(function ($day) { $day = intval($day); return $day === 0 ? 7 : $day; }, $days); } // Timezone handling if ( defined('ADVANCED_GUTENBERG_PRO_LOADED') && isset($schedule['timezone']) && !empty($schedule['timezone']) && method_exists('PPB_AdvancedGutenbergPro\Utils\Definitions', 'advgb_pro_set_timezone') ) { $timezone = \PPB_AdvancedGutenbergPro\Utils\Definitions::advgb_pro_set_timezone( esc_html($schedule['timezone']) ); } else { $timezone = wp_timezone(); } // Start showing if (!empty($schedule['dateFrom'])) { $dateFrom = \DateTime::createFromFormat('Y-m-d\TH:i:s', $schedule['dateFrom'], $timezone); // Reset seconds to zero to enable proper comparison $dateFrom->setTime($dateFrom->format('H'), $dateFrom->format('i'), 0); } // Stop showing if (!empty($schedule['dateTo'])) { $dateTo = \DateTime::createFromFormat('Y-m-d\TH:i:s', $schedule['dateTo'], $timezone); // Reset seconds to zero to enable proper comparison $dateTo->setTime($dateTo->format('H'), $dateTo->format('i'), 0); if ($dateFrom) { // Recurring is only relevant when both dateFrom and dateTo are defined $recurring = isset($schedule['recurring']) ? $schedule['recurring'] : false; } } /** * Time handling * Time from and Time to exists and are valid. * Valid times: "02:00:00", "19:35:00" * Invalid times: "-06:00:00", "25:00:00" * */ if ( !empty($schedule['timeFrom']) && !empty($schedule['timeTo']) && strtotime($schedule['timeFrom']) !== false && strtotime($schedule['timeTo']) !== false ) { // Get current datetime with timezone $timeNow = new \DateTime('now', $timezone); $timeNow->format('Y-m-d\TH:i:s'); $timeFrom = clone $timeNow; $timeTo = clone $timeNow; // Replace with our time attributes in previously generated datetime $timeFrom->modify($schedule['timeFrom']); $timeTo->modify($schedule['timeTo']); } if ($dateFrom || $dateTo || $days || ($timeFrom && $timeTo)) { // Fetch current time keeping in mind the timezone $now = new \DateTime('now', $timezone); $now->format('Y-m-d\TH:i:s'); /* Reset seconds to zero to enable proper comparison * as the from and to dates have those as 0 * but do this only for the from comparison * as we need the block to stop showing at the right time and not 1 minute extra */ $nowFrom = clone $now; $nowFrom->setTime($now->format('H'), $now->format('i'), 0); // Decide if block is displayed or not if ($recurring) { // Make the year same as today's $dateFrom->setDate( $nowFrom->format('Y'), $dateFrom->format('m'), $dateFrom->format('j') ); $dateTo->setDate($nowFrom->format('Y'), $dateTo->format('m'), $dateTo->format('j')); } if ( (!$schedule['dateFrom'] || $dateFrom->getTimestamp() <= $nowFrom->getTimestamp()) // No "Start showing", or "Start showing" <= Now && (!$schedule['dateTo'] || $now->getTimestamp() < $dateTo->getTimestamp()) // No "Stop showing", or now < "Stop showing" && && (!count($days) || in_array($nowFrom->format('N'), $days)) // "These days" && (!$schedule['timeFrom'] || $timeFrom->getTimestamp() <= $nowFrom->getTimestamp()) // No "Time from", or "Time && (!$schedule['timeTo'] || $now->getTimestamp() < $timeTo->getTimestamp()) // No "Time to", or now ) { // If any schedule matches, show the block $shouldShow = true; return $shouldShow; } } } return $shouldShow; break; // User role control case 'user_role': $bControl = $block['attrs']['advgbBlockControls'][$key]; $selected_roles = is_array($bControl['roles']) && count($bControl['roles']) ? $bControl['roles'] : []; if (count($selected_roles)) { // Check if user role exists to avoid non-valid roles foreach ($selected_roles as $key => $role) { if (!$GLOBALS['wp_roles']->is_role($role)) { unset($selected_roles[$key]); } } } // Check current user role visit $user = wp_get_current_user(); $approach = isset($bControl['approach']) && !empty(sanitize_text_field($bControl['approach'])) ? $bControl['approach'] : 'public'; switch ($approach) { default: case 'public': return true; break; case 'hidden': return false; break; case 'login': return is_user_logged_in() ? true : false; break; case 'logout': return !is_user_logged_in() ? true : false; break; case 'include': return array_intersect($selected_roles, $user->roles) ? true : false; break; case 'exclude': return !array_intersect($selected_roles, $user->roles) ? true : false; break; } break; case 'device_type': $bControl = $block['attrs']['advgbBlockControls'][$key]; $selected_devices = isset($bControl['devices']) ? $bControl['devices'] : []; // If no devices selected, show on all if (empty($selected_devices)) { return true; } if ($device_type === null) { $device_type = self::getDeviceType(); } // Use the pre-detected device type return in_array($device_type, $selected_devices); break; case 'device_width': $bControl = $block['attrs']['advgbBlockControls'][$key]; $min_width = isset($bControl['min_width']) ? intval($bControl['min_width']) : 0; $max_width = isset($bControl['max_width']) ? intval($bControl['max_width']) : 0; // Get current screen width $screen_width = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? (isset($_SERVER['HTTP_X_SCREEN_WIDTH']) ? intval($_SERVER['HTTP_X_SCREEN_WIDTH']) : 0) : (isset($_SERVER['HTTP_X_CLIENT_WIDTH']) ? intval($_SERVER['HTTP_X_CLIENT_WIDTH']) : 0); // If we couldn't detect screen width, return true if ($screen_width === 0) { return true; } // Check min/max width $min_ok = ($min_width === 0) || ($screen_width >= $min_width); $max_ok = ($max_width === 0) || ($screen_width <= $max_width); return $min_ok && $max_ok; break; case 'browser_device': return self::checkBrowserDevice($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'operating_system': return self::checkOperatingSystem($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'cookie': return self::checkCookie($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'user_meta': return self::checkUserMeta($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'post_meta': return self::checkPostMeta($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'query_string': return self::checkQueryString($block, $block['attrs']['advgbBlockControls'][$key]); break; case 'capabilities': return self::checkCapabilities($block, $block['attrs']['advgbBlockControls'][$key]); break; // Archive control case 'archive': $bControl = $block['attrs']['advgbBlockControls'][$key]; $taxonomies = is_array($bControl['taxonomies']) ? $bControl['taxonomies'] : []; $taxQuery = get_queried_object(); if (!isset($taxQuery->taxonomy)) { return true; } $merged_tax = []; // To store selected taxonomies. e.g. [ 'category', 'post_tag' ] $merged_terms = []; // To store selected terms from all taxonomies. e.g. [99,72,51] // Create taxonomies array if (isset($taxonomies) && count($taxonomies)) { foreach ($taxonomies as $item) { $merged_tax[] = sanitize_text_field($item['tax']); // Create terms array if (isset($item['terms']) && count($item['terms'])) { foreach ($item['terms'] as $term) { $merged_terms[] = intval($term); } } } } if (count($merged_tax)) { $approach = isset($bControl['approach']) && !empty(sanitize_text_field($bControl['approach'])) ? $bControl['approach'] : 'exclude'; switch ($approach) { case 'include': return self::checkTaxonomies($taxonomies, $merged_terms, $taxQuery) ? true : false; break; case 'exclude': return self::checkTaxonomies($taxonomies, $merged_terms, $taxQuery) ? false : true; break; } } break; // Pages control case 'page': $bControl = $block['attrs']['advgbBlockControls'][$key]; $selected = is_array($bControl['pages']) ? $bControl['pages'] : []; if (count($selected)) { $selected = array_map('sanitize_text_field', $selected); $approach = isset($bControl['approach']) && !empty(sanitize_text_field($bControl['approach'])) ? $bControl['approach'] : 'public'; switch ($approach) { default: case 'public': return true; break; case 'include': return self::checkPages($selected) ? true : false; break; case 'exclude': return self::checkPages($selected) ? false : true; break; } } break; } return true; } public static function getBrowserType() { static $browser_type = null; if ($browser_type === null) { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $dd = new DeviceDetector($user_agent); $dd->parse(); $browser_type = strtolower($dd->getClient('name')); } return $browser_type; } public static function getOperatingSystem() { static $os_type = null; if ($os_type === null) { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $dd = new DeviceDetector($user_agent); $dd->parse(); $os_type = strtolower($dd->getOs('name')); } return $os_type; } /** * Get device type * * @return string */ public static function getDeviceType() { static $device_type = null; if ($device_type === null) { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $dd = new DeviceDetector($user_agent); $dd->parse(); if ($dd->isBot()) { $device_type = 'robot'; } elseif ($dd->isMobile()) { $device_type = 'mobile'; } elseif ($dd->isTablet()) { $device_type = 'tablet'; } else { $device_type = 'desktop'; } } return $device_type; } public static function checkBrowserDevice($block, $control_data) { $browser = self::getBrowserType(); $browsers = isset($control_data['browsers']) ? $control_data['browsers'] : []; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; $match = in_array($browser, $browsers); return ($approach === 'include') ? $match : !$match; } public static function checkOperatingSystem($block, $control_data) { $os = self::getOperatingSystem(); $systems = isset($control_data['systems']) ? $control_data['systems'] : []; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; $match = in_array($os, $systems); return ($approach === 'include') ? $match : !$match; } public static function checkCookie($block, $control_data) { $cookie_name = isset($control_data['name']) ? $control_data['name'] : ''; $condition = isset($control_data['condition']) ? $control_data['condition'] : '='; $value = isset($control_data['value']) ? $control_data['value'] : ''; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; if (empty($cookie_name)) return false; $cookie_value = isset($_COOKIE[$cookie_name]) ? $_COOKIE[$cookie_name] : null; $match = self::evaluateCondition($cookie_value, $condition, $value); return ($approach === 'include') ? $match : !$match; } public static function checkUserMeta($block, $control_data) { if (!is_user_logged_in()) return false; $meta_key = isset($control_data['key']) ? $control_data['key'] : ''; $condition = isset($control_data['condition']) ? $control_data['condition'] : '='; $value = isset($control_data['value']) ? $control_data['value'] : ''; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; if (empty($meta_key)) return false; $meta_value = get_user_meta(get_current_user_id(), $meta_key, true); $match = self::evaluateCondition($meta_value, $condition, $value); return ($approach === 'include') ? $match : !$match; } public static function checkPostMeta($block, $control_data) { global $post; if (!$post) return false; $meta_key = isset($control_data['key']) ? $control_data['key'] : ''; $condition = isset($control_data['condition']) ? $control_data['condition'] : '='; $value = isset($control_data['value']) ? $control_data['value'] : ''; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; if (empty($meta_key)) return false; $meta_value = get_post_meta($post->ID, $meta_key, true); $match = self::evaluateCondition($meta_value, $condition, $value); return ($approach === 'include') ? $match : !$match; } public static function checkQueryString($block, $control_data) { $query_strings = isset($control_data['queries']) ? $control_data['queries'] : []; if (!is_array($query_strings)) { $query_strings = array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', $query_strings ) ) ); } $logic = isset($control_data['logic']) ? $control_data['logic'] : 'all'; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; if (empty($query_strings)) return false; $matches = 0; foreach ($query_strings as $query) { if (isset($_GET[$query])) { $matches++; } } $result = false; if ($logic === 'all') { $result = $matches === count($query_strings); } else { // 'any' $result = $matches > 0; } return ($approach === 'include') ? $result : !$result; } public static function checkCapabilities($block, $control_data) { if (! is_user_logged_in()) { return false; } $capabilities = isset($control_data['capabilities']) ? $control_data['capabilities'] : []; $approach = isset($control_data['approach']) ? $control_data['approach'] : 'include'; if (empty($capabilities)) { return false; } $has_capability = false; foreach ($capabilities as $cap) { if (current_user_can($cap)) { $has_capability = true; break; } } return ($approach === 'include') ? $has_capability : !$has_capability; } private static function evaluateCondition($actual, $condition, $expected) { switch ($condition) { case '=': return $actual == $expected; case '!=': return $actual != $expected; case '<': return $actual < $expected; case '>': return $actual > $expected; case '<=': return $actual <= $expected; case '>=': return $actual >= $expected; case 'contains': return strpos($actual, $expected) !== false; case 'beginsWith': return strpos($actual, $expected) === 0; case 'endsWith': return substr($actual, -strlen($expected)) === $expected; case 'doesNotContain': return strpos($actual, $expected) === false; case 'doesNotBeginWith': return strpos($actual, $expected) !== 0; case 'doesNotEndWith': return substr($actual, -strlen($expected)) !== $expected; case 'null': return is_null($actual) || $actual === ''; case 'notNull': return !is_null($actual) && $actual !== ''; case 'in': $values = explode(',', $expected); return in_array($actual, array_map('trim', $values)); case 'notIn': $values = explode(',', $expected); return !in_array($actual, array_map('trim', $values)); case 'between': $values = explode(',', $expected); if (count($values) === 2) { return $actual >= trim($values[0]) && $actual <= trim($values[1]); } return false; case 'notBetween': $values = explode(',', $expected); if (count($values) === 2) { return !($actual >= trim($values[0]) && $actual <= trim($values[1])); } return true; default: return false; } } /** * Generate a unique identifier for the block * * @param mixed $block * @return string */ private static function getBlockIdentifier($block) { // Use block ID if available if (isset($block['attrs']['block_id'])) { return 'advgb-block-' . $block['attrs']['block_id']; } // Fallback to hash of block content $content_hash = substr(md5($block['innerHTML']), 0, 8); return 'advgb-dyn-' . $content_hash; } /** * Add control class to block * * @param mixed $block_content * @param mixed $identifier * * @return mixed */ private static function addBlockControlClass($block_content, $identifier) { // Skip if already processed if (strpos($block_content, $identifier) !== false) { return $block_content; } // Handle blocks with existing class attribute if (strpos($block_content, 'class="') !== false) { return preg_replace( '/class="([^"]*)"/', 'class="$1 ' . esc_attr($identifier) . '"', $block_content, 1 ); } // Handle blocks without class attribute return preg_replace( '/<([a-zA-Z0-9_-]+)([^>]*)>/', '<$1$2 class="' . esc_attr($identifier) . '">', $block_content, 1 ); } /** * Additional css backup for device type and device width * @param mixed $block_content * @param mixed $block * @param string $identifier * */ public static function outputControlCSS($block_content, $block, $identifier) { if ( Utilities::settingIsEnabled('block_controls') && isset($block['attrs']['advgbBlockControls']) && $block['blockName'] ) { $controls = $block['attrs']['advgbBlockControls']; $css_rules = []; foreach ($controls as $key => $item) { if (isset($item['control']) && isset($item['enabled']) && (bool) $item['enabled'] === true) { $control_items = []; if ($item['control'] === 'presets') { $selected_presets = $item['selected'] ?? []; foreach ($selected_presets as $preset_id) { $preset_item_config = self::getPresetItemConfig($preset_id); $control_sets = $preset_item_config['controlSets'] ?? []; foreach ($control_sets as $control_set) { $control_set_rules = $control_set['rules'] ?? []; foreach ($control_set_rules as $control_set_rule) { /** * CSS Operator cannot respect AND/OR rules in presets. * So, we should 100% relied on PHP logic for 'device_type' * except 'device_width' that's solely css. */ if (in_array($control_set_rule['type'], ['device_width'])) { $control_set_rule['control'] = $control_set_rule['type']; $control_items[] = $control_set_rule; } } } } } else { $control_items[] = $item; } foreach ($control_items as $control_item) { switch ($control_item['control']) { case 'device_type': $devices = isset($control_item['devices']) ? $control_item['devices'] : []; if (!empty($devices)) { $hidden_devices = array_diff(['desktop', 'tablet', 'mobile'], $devices); foreach ($hidden_devices as $device) { switch ($device) { case 'mobile': $css_rules[] = "@media (max-width: 767px) { .{$identifier} { display: none !important; } }"; break; case 'tablet': $css_rules[] = "@media (min-width: 768px) and (max-width: 1024px) { .{$identifier} { display: none !important; } }"; break; case 'desktop': $css_rules[] = "@media (min-width: 1025px) { .{$identifier} { display: none !important; } }"; break; } } } break; case 'device_width': $min_width = isset($control_item['min_width']) ? intval($control_item['min_width']) : 0; $max_width = isset($control_item['max_width']) ? intval($control_item['max_width']) : 0; if ($min_width > 0) { $css_rules[] = "@media (max-width: " . ($min_width - 1) . "px) { .{$identifier} { display: none !important; } }"; } if ($max_width > 0) { $css_rules[] = "@media (min-width: " . ($max_width + 1) . "px) { .{$identifier} { display: none !important; } }"; } break; } } } } if (!empty($css_rules)) { $block_content = '' . $block_content; } } return $block_content; } /** * Check taxonomies in frontend * * @param array $taxonomies Array of taxonomies setup e.g. [ ['tax'=>'category', 'terms' => [172,99,3], 'all' => false], ['tax'=>'post_tag', 'terms' => [], 'all' => true] ] * @param array $terms Array of term ids e.g. [172,99,3] * @param object $taxQuery WP_Term * * @return bool * @since 3.1.2 * */ public static function checkTaxonomies($taxonomies, $terms, $taxQuery) { foreach ($taxonomies as $item) { if ((string) $item['tax'] === $taxQuery->taxonomy && (bool) $item['all']) { // Taxonomy found & all terms return true; } elseif (in_array($taxQuery->term_id, $terms)) { // Term found return true; } else { // Nothing to do here } } return false; } /** * Check pages in frontend * * @param array $selected Array of pages e.g. ['home', 'search'] * * @return bool * @since 3.1.1 * */ public static function checkPages($selected) { if ( in_array('home', $selected) && ((is_home() && is_front_page()) || is_front_page() ) ) { return true; } elseif (in_array('blog', $selected) && is_home()) { return true; } elseif (in_array('archive', $selected) && is_archive()) { return true; } elseif (in_array('search', $selected) && is_search()) { return true; } elseif (in_array('page404', $selected) && is_404()) { return true; } return false; } /** * Add attributes to ServerSideRender blocks to fix "Invalid parameter(s): attributes" error. * As example: 'core/latest-comments' * Related Gutenberg issue: https://github.com/WordPress/gutenberg/issues/16850 * * @since 3.1.0 function renamed and migrated from AdvancedGutenbergMain * @since 2.14.0 */ public static function addAttributes() { $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); foreach ($registered_blocks as $block) { $block->attributes['advgbBlockControls'] = [ 'type' => 'array', 'default' => [], ]; // Add custom style attributes to fix https://github.com/publishpress/publishpress-blocks/issues/1652 $block->attributes['customStyle'] = [ 'type' => 'string', ]; $block->attributes['identifyColor'] = [ 'type' => 'string', ]; } } /** * Make sure ServerSideRender blocks are rendererd correctly in editor. * As example: 'core/latest-comments' * https://github.com/brainstormforce/ultimate-addons-for-gutenberg/blob/master/classes/class-uagb-loader.php#L136-L194 * * @since 3.1.0 function renamed and migrated from AdvancedGutenbergMain * @since 2.14.0 */ public static function removeAttributes($result, $server, $request) { if (strpos($request->get_route(), '/wp/v2/block-renderer') !== false) { if (isset($request['attributes'])) { $attributes = $request['attributes']; if (isset($attributes['advgbBlockControls'])) { unset($attributes['advgbBlockControls']); } if (isset($attributes['customStyle'])) { unset($attributes['customStyle']); } if (isset($attributes['identifyColor'])) { unset($attributes['identifyColor']); } $request['attributes'] = $attributes; } } return $result; } /** * Get a Block control value from database option * * @param string $name Setting name - e.g. 'schedule' from advgb_block_controls > controls * @param bool $default Default value when $setting doesn't exist in $option * * @return bool * @since 3.1.0 * */ public static function getControlValue($name, $default) { $settings = get_option('advgb_block_controls'); $value = isset($settings['controls'][$name]) ? (bool) $settings['controls'][$name] : (bool) $default; return $value; } /** * Check if Block controls are enabled for a particular block * * @param string $block Block name. e.g. 'core/paragraph' * * @return bool * @since 3.1.0 * */ public static function isBlockEnabled($block) { $settings = get_option('advgb_block_controls'); if ( $settings && isset($settings['inactive_blocks']) && is_array($settings['inactive_blocks']) && count($settings['inactive_blocks']) > 0 && in_array($block, $settings['inactive_blocks']) ) { return false; } return true; } /** * Save block controls page data * * @return boolean true on success, false on failure * @since 3.1.0 */ public static function save() { if (!current_user_can('activate_plugins')) { return false; } // Controls if (isset($_POST['save_controls'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- we check nonce below if ( !wp_verify_nonce( sanitize_key(wp_unslash($_POST['advgb_controls_settings_nonce_field'])), 'advgb_controls_settings_nonce' ) ) { return false; } $advgb_block_controls = get_option('advgb_block_controls'); $advgb_block_controls['controls']['schedule'] = isset($_POST['schedule_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['user_role'] = isset($_POST['user_role_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['device_type'] = isset($_POST['device_type_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['device_width'] = isset($_POST['device_width_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['archive'] = isset($_POST['archive_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['page'] = isset($_POST['page_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['browser_device'] = isset($_POST['browser_device_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['operating_system'] = isset($_POST['operating_system_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['cookie'] = isset($_POST['cookie_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['user_meta'] = isset($_POST['user_meta_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['post_meta'] = isset($_POST['post_meta_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['query_string'] = isset($_POST['query_string_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['capabilities'] = isset($_POST['capabilities_control']) ? (bool) 1 : (bool) 0; $advgb_block_controls['controls']['presets'] = isset($_POST['presets_control']) ? (bool) 1 : (bool) 0; update_option('advgb_block_controls', $advgb_block_controls, false); wp_safe_redirect( add_query_arg( [ 'save' => 'success' ], str_replace( '/wp-admin/', '', sanitize_url(wp_unslash($_POST['_wp_http_referer'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ) ) ); exit; } // Blocks elseif (isset($_POST['save_blocks'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- we check nonce below if ( !wp_verify_nonce( sanitize_key(wp_unslash($_POST['advgb_controls_block_nonce_field'])), 'advgb_controls_block_nonce' ) ) { return false; } if ( isset($_POST['blocks_list']) && isset($_POST['active_blocks']) && is_array($_POST['active_blocks']) ) { $blocks_list = array_map( 'sanitize_text_field', json_decode(wp_unslash($_POST['blocks_list'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ); $active_blocks = array_map('sanitize_text_field', wp_unslash($_POST['active_blocks'])); $inactive_blocks = array_values(array_diff($blocks_list, $active_blocks)); // Save block controls $block_controls = get_option('advgb_block_controls'); $block_controls = is_array($block_controls) ? $block_controls : []; $block_controls['active_blocks'] = isset($active_blocks) ? $active_blocks : ''; $block_controls['inactive_blocks'] = isset($inactive_blocks) ? $inactive_blocks : ''; update_option('advgb_block_controls', $block_controls, false); // Redirect with success message wp_safe_redirect( add_query_arg( [ 'tab' => 'blocks', 'save' => 'success' ], str_replace( '/wp-admin/', '', sanitize_url(wp_unslash($_POST['_wp_http_referer'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ) ) ); exit; } else { // Redirect with error message / Nothing was saved wp_safe_redirect( add_query_arg( [ 'save' => 'error' ], str_replace( '/wp-admin/', '', sanitize_url(wp_unslash($_POST['_wp_http_referer'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ) ) ); exit; } } else { // Nothing to do here } return false; } /** * Get controls status (enabled/disabled) * * @return array e.g.['schedule' => true, 'user_role' => true] * @since 3.1.1 */ private static function getControlsArray() { $block_controls = get_option('advgb_block_controls'); $result = []; $controls = [ 'schedule', 'user_role', 'device_type', 'device_width', 'archive', 'page', 'browser_device', 'operating_system', 'cookie', 'user_meta', 'post_meta', 'query_string', 'capabilities', 'presets' ]; if ($block_controls) { foreach ($controls as $item) { $result[$item] = isset($block_controls['controls'][$item]) ? (bool) $block_controls['controls'][$item] : (bool) 1; } } else { foreach ($controls as $item) { $result[$item] = (bool) 1; } } return $result; } public static function getPresets($retain_key = false) { $presets = (array) get_option('advgb_block_control_presets', []); if ($retain_key) { return $presets; } $formatted_presets = []; foreach ($presets as $id => $preset) { $formatted_presets[] = array_merge($preset, ['id' => $id]); } return $formatted_presets; } public static function savePreset($preset_data) { $presets = self::getPresets(); $preset_id = $preset_data['id'] ?? uniqid('preset_'); $presets[$preset_id] = $preset_data; update_option('advgb_block_control_presets', $presets); return $preset_id; } public static function deletePreset($preset_id) { $presets = self::getPresets(); unset($presets[$preset_id]); update_option('advgb_block_control_presets', $presets); } /** * Javascript objects with controls configuration to load in Admin. * 'advgb_block_controls_vars' object with controls configuration. * 'advgb_blocks_list' object with all the saved blocks in 'advgb_blocks_list' option. * * @return void * @since 3.1.0 */ public static function adminData() { // Build blocks form and add filters functions wp_add_inline_script( 'advgb_main_js', "window.addEventListener('load', function () { advgbGetBlockControls( advgb_block_controls_vars.inactive_blocks, '#advgb_block_controls_nonce_field', 'advgb_block_controls', " . wp_json_encode(self::defaultExcludedBlocks()) . " ); });" ); do_action('enqueue_block_editor_assets'); // Block categories $blockCategories = array(); if (function_exists('get_block_categories')) { $blockCategories = get_block_categories(get_post()); } elseif (function_exists('gutenberg_get_block_categories')) { $blockCategories = gutenberg_get_block_categories(get_post()); } wp_add_inline_script( 'wp-blocks', sprintf('wp.blocks.setCategories( %s );', wp_json_encode($blockCategories)), 'after' ); // Block types $block_type_registry = \WP_Block_Type_Registry::get_instance(); foreach ($block_type_registry->get_all_registered() as $block_name => $block_type) { if (!empty($block_type->editor_script)) { wp_enqueue_script($block_type->editor_script); } } /* Get blocks saved in advgb_blocks_list option to include the ones that are missing * as result of javascript method wp.blocks.getBlockTypes() * e.g. blocks registered only via PHP */ $advgb_blocks_list = get_option('advgb_blocks_list'); if ($advgb_blocks_list && is_array($advgb_blocks_list)) { $saved_blocks = $advgb_blocks_list; } else { $saved_blocks = []; } wp_localize_script( 'advgb_main_js', 'advgb_blocks_list', $saved_blocks ); // Active and inactive blocks $block_controls = get_option('advgb_block_controls'); if ( $block_controls && isset($block_controls['active_blocks']) && isset($block_controls['inactive_blocks']) && is_array($block_controls['active_blocks']) && is_array($block_controls['inactive_blocks']) ) { wp_localize_script( 'wp-blocks', 'advgb_block_controls_vars', [ 'controls' => self::getControlsArray(), 'active_blocks' => $block_controls['active_blocks'], 'inactive_blocks' => $block_controls['inactive_blocks'], 'user_roles' => self::getUserRoles(), 'taxonomies' => self::getTaxonomies(), 'page' => self::getPages(), 'capabilities' => self::getAllCapabilities(), 'presets' => self::getPresets() ] ); } else { // Nothing saved in database for current user role. Set empty (access to all blocks) wp_localize_script( 'wp-blocks', 'advgb_block_controls_vars', [ 'controls' => self::getControlsArray(), 'active_blocks' => [], 'inactive_blocks' => [], 'user_roles' => self::getUserRoles(), 'taxonomies' => self::getTaxonomies(), 'page' => self::getPages(), 'capabilities' => self::getAllCapabilities(), 'presets' => self::getPresets() ] ); } } /** * Javascript objects with controls configuration to load in Editor. * 'advgb_block_controls_vars' object with controls configuration. * * @return void * @since 3.1.0 */ public static function editorData() { $advgb_block_controls = get_option('advgb_block_controls'); if ( $advgb_block_controls && isset($advgb_block_controls['inactive_blocks']) && is_array($advgb_block_controls['inactive_blocks']) && count($advgb_block_controls['inactive_blocks']) > 0 ) { // Merge non supported saved and manually defined blocks $non_supported = array_merge( $advgb_block_controls['inactive_blocks'], self::defaultExcludedBlocks() ); $non_supported = array_unique($non_supported); } else { // Non supported manually defined blocks $non_supported = self::defaultExcludedBlocks(); } // Output js variable wp_localize_script( 'wp-blocks', 'advgb_block_controls_vars', [ 'non_supported' => $non_supported, 'controls' => self::getControlsArray(), 'user_roles' => self::getUserRoles(), 'taxonomies' => self::getTaxonomies(), 'page' => self::getPages(), 'capabilities' => self::getAllCapabilities(), 'presets' => self::getPresets() ] ); } public static function getAllCapabilities() { global $wp_roles; if ( empty( $wp_roles->roles ) || ! is_array( $wp_roles->roles ) ) { return []; } $capability_arrays = array_map( function ($role) { return ! empty( $role['capabilities'] ) && is_array( $role['capabilities'] ) ? array_keys( $role['capabilities'] ) : []; }, $wp_roles->roles ); $capability_arrays = array_values( array_filter( $capability_arrays ) ); if ( empty( $capability_arrays ) ) { return []; } $merged = call_user_func_array( 'array_merge', $capability_arrays ); return array_values( array_unique( $merged ) ); } /** * Block controls support for these blocks is not available * * @return void * @since 3.1.0 */ public static function defaultExcludedBlocks() { return array_values(array_unique(array_merge( [ 'core/freeform', 'core/legacy-widget', 'core/widget-area', 'core/column', 'advgb/tab', 'advgb/column', 'advgb/accordion' // @TODO - Deprecated block. Remove later. ], \AdvancedGutenbergMain::allDisabledBlocks() ))); } /** * Enqueue assets for editor * * @param $wp_editor_dep Block editor dependency based on current screen. e.g. 'wp-editor' * * @return void * @since 3.1.0 * */ public static function editorAssets($wp_editor_dep) { if (Utilities::settingIsEnabled('block_controls')) { wp_enqueue_style('wp-components'); Utilities::enqueueToolTipsAssets(); wp_enqueue_style( 'advgb-preset-manager', ADVANCED_GUTENBERG_PLUGIN_DIR_URL . 'assets/css/block-controls-preset-manager.css', [], ADVANCED_GUTENBERG_VERSION ); wp_enqueue_script( 'advgb_preset_data_manager', ADVANCED_GUTENBERG_PLUGIN_DIR_URL . 'assets/pages/preset-data-manager.js', [ 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-data', 'wp-plugins', 'wp-compose' ], ADVANCED_GUTENBERG_VERSION, false ); wp_enqueue_script( 'advgb_block_controls', ADVANCED_GUTENBERG_PLUGIN_DIR_URL . 'assets/pages/block-controls.js', [ 'advgb_preset_data_manager', 'ppb-tooltips-js', 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-data', $wp_editor_dep, 'wp-plugins', 'wp-compose' ], ADVANCED_GUTENBERG_VERSION, true ); wp_enqueue_script( 'advgb-preset-manager', ADVANCED_GUTENBERG_PLUGIN_DIR_URL . 'assets/pages/preset-manager.js', ['advgb_preset_data_manager', 'wp-components', 'wp-element', 'wp-i18n', 'wp-api-fetch'], ADVANCED_GUTENBERG_VERSION, true ); wp_set_script_translations( 'advgb_preset_data_manager', 'advanced-gutenberg', plugin_dir_path(ADVANCED_GUTENBERG_PLUGIN) . 'languages' ); wp_set_script_translations( 'advgb-preset-manager', 'advanced-gutenberg', plugin_dir_path(ADVANCED_GUTENBERG_PLUGIN) . 'languages' ); wp_set_script_translations( 'advgb_block_controls', 'advanced-gutenberg', plugin_dir_path(ADVANCED_GUTENBERG_PLUGIN) . 'languages' ); } } /** * Retrieve User roles * * @return array * @since 3.1.0 * */ public static function getUserRoles() { global $wp_roles; $result = []; $roles_list = $wp_roles->get_names(); foreach ($roles_list as $roles => $role_name) { $result[] = [ 'slug' => $roles, 'title' => esc_attr(translate_user_role($role_name)) ]; } return $result; } /** * Retrieve Taxonomies * * @return array * @since 3.1.1 * */ public static function getTaxonomies() { $taxonomies = get_taxonomies(); $result = []; $exclude = [ 'nav_menu', 'link_category', 'post_format', 'wp_theme', 'wp_template_part_area' ]; foreach ($taxonomies as $item) { $tax = get_taxonomy($item); if (!in_array($item, $exclude)) { $result[] = [ 'slug' => $item, 'title' => $tax->labels->singular_name ]; } } return $result; } /** * Retrieve Taxonomies selected in a block * * @param array $selected Selected taxonomies in the block * * @return array * @since 3.1.1 * */ public static function getBlockTaxonomies($selected) { if (!is_array($selected) || !count($selected)) { return []; } global $wp_taxonomies; $result = []; $taxonomies = $selected; foreach ($wp_taxonomies as $key => $value) { if (in_array($key, $taxonomies)) { $result[] = [ 'slug' => $key, 'name' => $value->labels->singular_name ]; } } return $result; } /** * Retrieve Terms * * @param array $data Taxonomy slugs and term ids or search word * * @return array * @since 3.1.1 * */ public static function getTerms($data) { if ( isset($data['taxonomies']) && is_array($data['taxonomies']) && count($data['taxonomies']) ) { $taxonomies = array_map('sanitize_text_field', $data['taxonomies']); $args['taxonomy'] = $taxonomies; // Note: can't use search and include in the same request if (isset($data['search']) && !empty($data['search'])) { $args['search'] = sanitize_text_field($data['search']); $args['number'] = 10; } if (isset($data['ids']) && is_array($data['ids']) && count($data['ids'])) { $args['include'] = array_map('intval', $data['ids']); } $result = []; /*/ Include "All terms" options global $wp_taxonomies; foreach( $taxonomies as $tax ) { $result[] = [ 'slug' => "all__{$tax}", 'title' => sprintf( __( 'All %s terms', 'advanced-gutenberg' ), $wp_taxonomies[$tax]->labels->singular_name ), 'tax' => $tax, ]; }*/ $term_query = new \WP_Term_Query($args); if (!empty($term_query->terms)) { foreach ($term_query->terms as $term) { $taxLabel = $term->taxonomy; // Get human readable taxonomy name $blockTaxonomies = self::getBlockTaxonomies($taxonomies); if (count($blockTaxonomies)) { foreach ($blockTaxonomies as $tax) { if ($tax['slug'] === $term->taxonomy) { $taxLabel = $tax['name']; break; } } } $result[] = [ 'slug' => $term->term_id, 'title' => $term->name . ' (' . $taxLabel . ')', 'tax' => $term->taxonomy, ]; } } return $result; } return []; } /** * Retrieve pages * * @return array * @since 3.1.1 * */ public static function getPages() { return [ [ 'slug' => 'home', 'title' => __('Home', 'advanced-gutenberg') ], [ 'slug' => 'blog', 'title' => __('Blog', 'advanced-gutenberg') ], [ 'slug' => 'archive', 'title' => __('Archive', 'advanced-gutenberg') ], [ 'slug' => 'search', 'title' => __('Search', 'advanced-gutenberg') ], [ 'slug' => 'page404', 'title' => __('404', 'advanced-gutenberg') ] ]; } /** * Register custom REST API routes * * @return array * @since 3.1.1 * */ public static function registerCustomRoutes() { // Fetch searched terms from all selected taxonomies register_rest_route( 'advgb/v1', '/terms', [ 'methods' => 'GET', 'callback' => ['PublishPress\Blocks\Controls', 'getTerms'], 'args' => [ 'search' => [ 'validate_callback' => ['PublishPress\Blocks\Controls', 'validateString'], 'sanitize_callback' => 'sanitize_text_field', 'required' => false, 'type' => 'string' ], 'ids' => [ 'validate_callback' => ['PublishPress\Blocks\Controls', 'validateArray'], 'sanitize_callback' => ['PublishPress\Blocks\Controls', 'sanitizeNumbersArray'], 'required' => false, 'type' => 'array' ], 'taxonomies' => [ 'validate_callback' => ['PublishPress\Blocks\Controls', 'validateArray'], 'sanitize_callback' => ['PublishPress\Blocks\Controls', 'sanitizeStringsArray'], 'required' => true, 'type' => 'array' ], ], 'permission_callback' => function () { return current_user_can('edit_others_posts'); } ] ); } /** * Check if value is a string * * @param $value Value to check * * @return array * @since 3.1.1 * */ public static function validateString($value) { return is_string($value); } /** * Check if value is an array * * @param $value Value to check * * @return array * @since 3.1.1 * */ public static function validateArray($value) { return is_array($value) && count($value); } /** * Sanitize an array of strings * * @param $value Value to check * * @return array * @since 3.1.1 * */ public static function sanitizeStringsArray($value) { return array_map('sanitize_key', $value); } /** * Sanitize an array of numbers * * @param $value Value to check * * @return array * @since 3.1.1 * */ public static function sanitizeNumbersArray($value) { return array_map('intval', $value); } } }