文件操作 - helpers.php
返回文件管理
返回主菜单
删除本文件
文件: /storage/v12552/stanleysteamer/public_html/wp-content/themes/bricks/includes/helpers.php
编辑文件内容
<?php namespace Bricks; if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly class Helpers { /** * Get template data from post meta * * @since 1.0 */ public static function get_template_settings( $post_id ) { return get_post_meta( $post_id, BRICKS_DB_TEMPLATE_SETTINGS, true ); } /** * Store template settings * * @since 1.0 */ public static function set_template_settings( $post_id, $settings ) { update_post_meta( $post_id, BRICKS_DB_TEMPLATE_SETTINGS, $settings ); } /** * Remove template settings from store * * @since 1.0 */ public static function delete_template_settings( $post_id ) { delete_post_meta( $post_id, BRICKS_DB_TEMPLATE_SETTINGS ); } /** * Get individual template setting by key * * @since 1.0 */ public static function get_template_setting( $key, $post_id ) { $template_settings = self::get_template_settings( $post_id ); return isset( $template_settings[ $key ] ) ? $template_settings[ $key ] : ''; } /** * Store a specific template setting * * @since 1.0 */ public static function set_template_setting( $post_id, $key, $setting_value ) { $template_settings = self::get_template_settings( $post_id ); if ( ! is_array( $template_settings ) ) { $template_settings = []; } $template_settings[ $key ] = $setting_value; self::set_template_settings( $post_id, $template_settings ); } /** * Save ACF default values for a newly created post. * * @since 2.3.6 * * @param int $post_id The post ID. * * @return bool */ public static function maybe_save_acf_default_values( $post_id ) { $post_id = absint( $post_id ); if ( ! $post_id || ! function_exists( 'acf_get_field_groups' ) || ! function_exists( 'acf_get_fields' ) || ! function_exists( 'acf_save_post' ) ) { return false; } $post_type = get_post_type( $post_id ); if ( ! $post_type ) { return false; } $field_groups = acf_get_field_groups( [ 'post_id' => $post_id, 'post_type' => $post_type, ] ); if ( empty( $field_groups ) || ! is_array( $field_groups ) ) { return false; } $values = []; foreach ( $field_groups as $field_group ) { $fields = acf_get_fields( $field_group ); if ( empty( $fields ) || ! is_array( $fields ) ) { continue; } foreach ( $fields as $field ) { if ( empty( $field['key'] ) || empty( $field['name'] ) || metadata_exists( 'post', $post_id, $field['name'] ) ) { continue; } $default_value = self::get_acf_default_save_value( $field ); if ( $default_value['has_value'] ) { $values[ $field['key'] ] = $default_value['value']; } } } if ( empty( $values ) ) { return false; } // phpcs:disable WordPress.Security.NonceVerification.Missing -- Preserving ACF's existing save payload. $has_posted_acf = array_key_exists( 'acf', $_POST ); $posted_acf_data = $has_posted_acf ? $_POST['acf'] : null; acf_save_post( $post_id, $values ); if ( $has_posted_acf ) { $_POST['acf'] = $posted_acf_data; } else { unset( $_POST['acf'] ); } // phpcs:enable WordPress.Security.NonceVerification.Missing return true; } /** * Get the default value payload for saving an ACF field. * * @since 2.3.6 * * @param array $field ACF field settings. * * @return array */ private static function get_acf_default_save_value( $field ) { $response = [ 'has_value' => false, 'value' => null, ]; if ( empty( $field ) || ! is_array( $field ) || empty( $field['key'] ) ) { return $response; } $field_type = $field['type'] ?? ''; if ( $field_type === 'group' ) { return self::get_acf_group_default_save_value( $field, $response ); } if ( $field_type === 'repeater' ) { return self::get_acf_repeater_default_save_value( $field, $response ); } // Flexible content has no deterministic default layout to initialize. if ( $field_type === 'flexible_content' ) { return $response; } if ( ! array_key_exists( 'default_value', $field ) ) { return $response; } $default_value = $field['default_value']; if ( ! self::is_acf_default_value_meaningful( $default_value, $field ) ) { return $response; } $response['has_value'] = true; $response['value'] = $default_value; return $response; } /** * Get the default value payload for saving an ACF group field. * * @since 2.3.6 * * @param array $field ACF field settings. * @param array $response Default response. * * @return array */ private static function get_acf_group_default_save_value( $field, $response ) { if ( empty( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) { return $response; } $value = []; foreach ( $field['sub_fields'] as $sub_field ) { if ( empty( $sub_field['key'] ) ) { continue; } $default_value = self::get_acf_default_save_value( $sub_field ); if ( $default_value['has_value'] ) { $value[ $sub_field['key'] ] = $default_value['value']; } } if ( empty( $value ) ) { return $response; } $response['has_value'] = true; $response['value'] = $value; return $response; } /** * Get the default value payload for saving an ACF repeater field. * * @since 2.3.6 * * @param array $field ACF field settings. * @param array $response Default response. * * @return array */ private static function get_acf_repeater_default_save_value( $field, $response ) { $min_rows = ! empty( $field['min'] ) ? absint( $field['min'] ) : 0; if ( ! $min_rows || empty( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) { return $response; } $row = []; foreach ( $field['sub_fields'] as $sub_field ) { if ( empty( $sub_field['key'] ) ) { continue; } $default_value = self::get_acf_default_save_value( $sub_field ); if ( $default_value['has_value'] ) { $row[ $sub_field['key'] ] = $default_value['value']; } } if ( empty( $row ) ) { return $response; } $value = []; for ( $i = 0; $i < $min_rows; $i++ ) { $value[] = $row; } $response['has_value'] = true; $response['value'] = $value; return $response; } /** * Check if an ACF default value should be persisted. * * @since 2.3.6 * * @param mixed $value Default value. * @param array $field ACF field settings. * * @return bool */ private static function is_acf_default_value_meaningful( $value, $field = [] ) { $field_type = $field['type'] ?? ''; if ( $field_type === 'true_false' ) { // ACF persists explicit off defaults via the field's hidden input during normal saves. return $value !== null && $value !== '' && ! is_array( $value ); } if ( $value === null || $value === '' || $value === false ) { return false; } if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( self::is_acf_default_value_meaningful( $item ) ) { return true; } } return false; } return true; } /** * Get terms * * @param string $taxonomy Taxonomy name. * @param string $post_type Post type name. * @param string $include_all Includes meta terms like "All terms (taxonomy name)". * @param string $search Search term. (@since 1.12) * @since 1.0 */ public static function get_terms_options( $taxonomy = null, $post_type = null, $include_all = false, $search = '' ) { // Always limit to 100 terms for performance reasons (@since 1.12) $term_args = [ 'hide_empty' => false, 'number' => 100, 'lang' => '', // Get all terms in builder for Polylang (@since 2.0) ]; if ( isset( $taxonomy ) ) { $term_args['taxonomy'] = $taxonomy; } $search = trim( $search ); if ( ! empty( $search ) ) { $term_args['search'] = $search; } $cache_key = 'get_terms_options' . md5( 'taxonomy' . wp_json_encode( $taxonomy ) . 'post_type' . wp_json_encode( $post_type ) . 'include' . $include_all . 'search' . $search ); $response = wp_cache_get( $cache_key, 'bricks' ); if ( $response !== false ) { return $response; } /** * New include_all logic * Should not get from get_terms() since we are searching by search term. We can use this to exclude non-registered taxonomies terms too. * * @since 1.12 */ $all_taxonomies = get_taxonomies( [], 'objects' ); // We only need the labels and names $all_taxonomies = array_map( function( $tax ) { return [ 'name' => $tax->name, 'label' => $tax->label ]; }, $all_taxonomies ); // Allow exlucde taxonomies from the list (@since 2.0) $excluded_taxonomies = (array) apply_filters( 'bricks/get_terms_options/excluded_taxonomies', [ 'nav_menu', 'link_category', 'post_format', // BRICKS_DB_TEMPLATE_TAX_TAG ] ); // Filter out excluded taxonomies $all_taxonomies = array_filter( $all_taxonomies, static function( $tax ) use ( $excluded_taxonomies ) { return ! in_array( $tax['name'], $excluded_taxonomies, true ); } ); // Extract valid taxonomy slugs $valid_tax_slugs = array_column( $all_taxonomies, 'name' ); if ( ! isset( $taxonomy ) ) { // (Undocumented) For large site with humongous amount of taxonomies, but don't want to increase Memory Limit $enable_limit = apply_filters( 'bricks/get_terms_options/enable_limit', false ); if ( $enable_limit ) { /** * Get the first 20 taxonomies if there are more than 20 * * Gradually increase the limit based on the search term length and cap it to 250 * * Potential issues: * If all terms with a very short slug, and the taxonomy is not in the first N taxonomies, can't get the expected term. No ideal solution for this yet. */ $limit = 20; $search_length = isset( $term_args['search'] ) ? strlen( $search ) : 0; // if has search term, can increase the included taxonomies gradually (avoid memory exhaustion) if ( $search_length >= 3 ) { // If more than 3 characters, increase the limit $limit = 100 + ( ( $search_length - 3 ) * 50 ); } if ( count( $valid_tax_slugs ) > $limit ) { $valid_tax_slugs = array_slice( $valid_tax_slugs, 0, $limit ); } } // Always set the taxonomy parameter $term_args['taxonomy'] = $valid_tax_slugs; } $terms = get_terms( $term_args ); if ( is_wp_error( $terms ) ) { return []; } $response = []; $all_terms = []; foreach ( $terms as $term ) { // Ensure term is an object if ( is_array( $term ) ) { $term = (object) $term; } if ( ! isset( $term->taxonomy ) ) { continue; } // Skip term if term taxonomy is not a taxonomy of requested post type if ( isset( $post_type ) && $post_type !== 'any' ) { $post_type_taxonomies = get_object_taxonomies( $post_type ); if ( ! in_array( $term->taxonomy, $post_type_taxonomies ) ) { continue; } } // Skip if the term is not a valid taxonomy (Maybe a custom taxonomy that is not registered after a plugin/theme deactivation) if ( ! in_array( $term->taxonomy, $valid_tax_slugs, true ) ) { continue; } // Store taxonomy name and term ID as WP_Query tax_query needs both (name and term ID) $taxonomy_label = self::generate_taxonomy_label( $term->taxonomy ); $taxonomy_label = ! empty( $taxonomy_label ) ? "($taxonomy_label)" : ''; $response[ $term->taxonomy . '::' . $term->term_id ] = "{$term->name} {$taxonomy_label}"; } if ( $include_all ) { // Build "All terms" option from all taxonomies foreach ( $all_taxonomies as $taxonomy ) { $all_terms[ $taxonomy['name'] . '::all' ] = esc_html__( 'All terms', 'bricks' ) . ' (' . $taxonomy['label'] . ')'; } $response = array_merge( $all_terms, $response ); } wp_cache_set( $cache_key, $response, 'bricks', 5 * MINUTE_IN_SECONDS ); return $response; } /** * Get users (for templatePreview) * * @param array $args Query args. * @param bool $show_role Show user role. * * @uses templatePreviewAuthor * * @since 1.0 */ public static function get_users_options( $args, $show_role = false ) { $users = []; foreach ( get_users( $args ) as $user ) { $user_id = $user->ID; $user_roles = array_values( $user->roles ); $value = get_the_author_meta( 'display_name', $user_id ); if ( $show_role && ! empty( $user_roles[0] ) ) { global $wp_roles; $value .= ' (' . $wp_roles->roles[ $user_roles[0] ]['name'] . ')'; } $users[ $user_id ] = $value; } return $users; } /** * Get post edit link with appended query string to trigger builder * * @since 1.0 */ public static function get_builder_edit_link( $post_id = 0 ) { if ( ! $post_id ) { $post_id = get_the_ID(); } $url = get_permalink( $post_id ); $url = add_query_arg( BRICKS_BUILDER_PARAM, 'run', $url ); // NOTE: Undocumented filter (@since 1.10) return apply_filters( 'bricks/get_builder_edit_link', $url, $post_id ); } /** * Get supported post types * * @since 1.0 * * @return array */ public static function get_supported_post_types() { $supported_post_types = Database::get_setting( 'postTypes', [] ); $post_types_options = []; foreach ( $supported_post_types as $post_type_slug ) { if ( $post_type_slug === 'attachment' ) { continue; } $post_type_object = get_post_type_object( $post_type_slug ); $post_types_options[ $post_type_slug ] = is_object( $post_type_object ) ? $post_type_object->labels->name : ucwords( str_replace( '_', ' ', $post_type_slug ) ); } return $post_types_options; } /** * Get registered post types * * Key: Post type name * Value: Post type label * * @since 1.0 * * @return array */ public static function get_registered_post_types() { /** * Hook to customise post type arguments * * Example: Return all registered post types, instead of only 'public' post types. * * https://academy.bricksbuilder.io/article/filter-bricks-registered_post_types_args/ * * @since 1.6 */ $registered_post_types_args = apply_filters( 'bricks/registered_post_types_args', [ 'public' => true, ] ); $registered_post_types = get_post_types( $registered_post_types_args, 'objects' ); // Remove post type: Bricks template (always has builder support) unset( $registered_post_types[ BRICKS_DB_TEMPLATE_SLUG ] ); $post_types = []; foreach ( $registered_post_types as $key => $object ) { $post_types[ $key ] = $object->labels->singular_name ?? $object->label; } return $post_types; } /** * Is current post type supported by builder * * @since 1.0 * * @return boolean */ public static function is_post_type_supported( $post_id = 0 ) { $post_id = ! empty( $post_id ) ? $post_id : get_the_ID(); // NOTE: Set post ID to posts page. if ( empty( $post_id ) && is_home() ) { $post_id = get_option( 'page_for_posts' ); } $current_post_type = get_post_type( $post_id ); // Bricks templates always have builder support if ( $current_post_type === BRICKS_DB_TEMPLATE_SLUG ) { return true; } $supported_post_types = Database::get_setting( 'postTypes', [] ); return in_array( $current_post_type, $supported_post_types ); } /** * Return page-specific title * * @param int $post_id * @param bool $context * * @see https://developer.wordpress.org/reference/functions/get_the_archive_title/ * * @since 1.0 * * @return string */ public static function get_the_title( $post_id = 0, $context = false ) { if ( ! $post_id ) { $post_id = get_the_ID(); } $preview_type = ''; // Check if loading a Bricks template if ( self::is_bricks_template( $post_id ) ) { $preview_type = self::get_template_setting( 'templatePreviewType', $post_id ); if ( $preview_type === 'archive-term' ) { $preview_term = self::get_template_setting( 'templatePreviewTerm', $post_id ); if ( ! empty( $preview_term ) ) { $preview_term = explode( '::', $preview_term ); $preview_taxonomy = isset( $preview_term[0] ) ? $preview_term[0] : ''; $preview_term_id = isset( $preview_term[1] ) ? intval( $preview_term[1] ) : ''; $preview_term = get_term_by( 'id', $preview_term_id, $preview_taxonomy ); $preview_term_name = $preview_term ? $preview_term->name : ''; } } elseif ( $preview_type == 'archive-cpt' ) { $preview_post_type = self::get_template_setting( 'templatePreviewPostType', $post_id ); } } if ( Query::is_looping() ) { if ( Query::get_loop_object_type() === 'post' ) { // Looping post query we can retrieve the title via $post_id $title = get_the_title( $post_id ); } else { // Looping but don't use $post_id as it might be a term id, author id, etc. (@since 1.11) (#86c0arcxn) $title = get_the_title(); } } elseif ( is_home() ) { $post_id = get_option( 'page_for_posts' ); $title = get_the_title( $post_id ); } elseif ( is_404() ) { $title = isset( Database::$active_templates['error'] ) ? get_the_title( Database::$active_templates['error'] ) : esc_html__( 'Page not found', 'bricks' ); } elseif ( is_category() || ( isset( $preview_taxonomy ) && $preview_taxonomy === 'category' ) ) { $category = isset( $preview_term_name ) ? $preview_term_name : single_cat_title( '', false ); $category = apply_filters( 'single_cat_title', $category ); // translators: %s: Category name $title = $context ? sprintf( esc_html__( 'Category: %s', 'bricks' ), $category ) : $category; } elseif ( is_tag() || ( isset( $preview_taxonomy ) && $preview_taxonomy === 'post_tag' ) ) { $tag = isset( $preview_term_name ) ? $preview_term_name : single_tag_title( '', false ); $tag = apply_filters( 'single_tag_title', $tag ); // translators: %s: Tag name $title = $context ? sprintf( esc_html__( 'Tag: %s', 'bricks' ), $tag ) : $tag; } elseif ( is_author() || $preview_type === 'archive-author' ) { if ( $preview_type === 'archive-author' ) { // Get author ID from template preview (as no $authordata exists) $template_preview_author = self::get_template_setting( 'templatePreviewAuthor', $post_id ); $author = get_the_author_meta( 'display_name', $template_preview_author ); } else { // @since 1.7.1 - get_the_author() might be wrong if some other query is running on the author archive page $author = get_the_author_meta( 'display_name', $post_id ); } $author = ! empty( $author ) ? $author : ''; // translators: %s: Author name $title = $context ? sprintf( esc_html__( 'Author: %s', 'bricks' ), $author ) : $author; } elseif ( is_year() || $preview_type === 'archive-date' ) { $date = $preview_type === 'archive-date' ? date( 'Y' ) : get_the_date( _x( 'Y', 'yearly archives date format' ) ); // translators: %s: Year $title = $context ? sprintf( esc_html__( 'Year: %s', 'bricks' ), $date ) : $date; } elseif ( is_month() ) { $date = get_the_date( _x( 'F Y', 'monthly archives date format' ) ); // translators: %s: Month $title = $context ? sprintf( esc_html__( 'Month: %s', 'bricks' ), $date ) : $date; } elseif ( is_day() ) { $date = get_the_date( _x( 'F j, Y', 'daily archives date format' ) ); // translators: %s: Day $title = $context ? sprintf( esc_html__( 'Day: %s', 'bricks' ), $date ) : $date; } elseif ( is_tax( 'post_format' ) ) { if ( is_tax( 'post_format', 'post-format-aside' ) ) { $title = esc_html__( 'Asides', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-gallery' ) ) { $title = esc_html__( 'Galleries', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-image' ) ) { $title = esc_html__( 'Images', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-video' ) ) { $title = esc_html__( 'Videos', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-quote' ) ) { $title = esc_html__( 'Quotes', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-link' ) ) { $title = esc_html__( 'Links', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-status' ) ) { $title = esc_html__( 'Statuses', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-audio' ) ) { $title = esc_html__( 'Audio', 'bricks' ); } elseif ( is_tax( 'post_format', 'post-format-chat' ) ) { $title = esc_html__( 'Chats', 'bricks' ); } } elseif ( is_tax() || isset( $preview_taxonomy ) ) { $tax = isset( $preview_taxonomy ) ? $preview_taxonomy : get_queried_object()->taxonomy; $tax = get_taxonomy( $tax ); $term = isset( $preview_term_name ) ? $preview_term_name : single_term_title( '', false ); $term = apply_filters( 'single_term_title', $term ); $title = $context ? $tax->labels->singular_name . ': ' . $term : $term; } elseif ( is_post_type_archive() || ! empty( $preview_post_type ) ) { // Check if post type actually exists (@since 1.9.2) $post_type_obj = ! empty( $preview_post_type ) ? get_post_type_object( $preview_post_type ) : false; if ( $post_type_obj ) { $post_type_archive_title = apply_filters( 'post_type_archive_title', $post_type_obj->labels->name, $preview_post_type ); } else { $post_type_archive_title = post_type_archive_title( '', false ); } // translators: %s: Post type archive title $title = $context ? sprintf( esc_html__( 'Archives: %s', 'bricks' ), $post_type_archive_title ) : $post_type_archive_title; } elseif ( is_search() || $preview_type === 'search' ) { $search_query = $preview_type === 'search' ? self::get_template_setting( 'templatePreviewSearchTerm', $post_id ) : get_search_query(); // translators: %s: Search query $title = $context ? sprintf( esc_html__( 'Results for: %s', 'bricks' ), $search_query ) : $search_query; if ( get_query_var( 'paged' ) ) { // translators: %s: Page number $title .= ' - ' . sprintf( esc_html__( 'Page %s', 'bricks' ), get_query_var( 'paged' ) ); } } else { $preview_id = self::get_template_setting( 'templatePreviewPostId', $post_id ); $preview_id = ! empty( $preview_id ) ? $preview_id : $post_id; $title = get_the_title( $preview_id ); } // NOTE: Undocumented return apply_filters( 'bricks/get_the_title', $title, $post_id ); } /** * Get the queried object which could also be set if previewing a template * * @see: https://developer.wordpress.org/reference/functions/get_queried_object/ * * @param int $post_id * * @return WP_Term|WP_User|WP_Post|WP_Post_Type */ public static function get_queried_object( $post_id ) { $looping_query_id = Query::is_any_looping(); $queried_object = ''; // Check if loading a Bricks template if ( self::is_bricks_template( $post_id ) ) { $preview_type = self::get_template_setting( 'templatePreviewType', $post_id ); if ( $preview_type == 'single' ) { $preview_id = self::get_template_setting( 'templatePreviewPostId', $post_id ); $queried_object = get_post( $preview_id ); } elseif ( $preview_type === 'archive-term' ) { $preview_term = self::get_template_setting( 'templatePreviewTerm', $post_id ); if ( ! empty( $preview_term ) ) { $preview_term = explode( '::', $preview_term ); $preview_taxonomy = isset( $preview_term[0] ) ? $preview_term[0] : ''; $preview_term_id = isset( $preview_term[1] ) ? intval( $preview_term[1] ) : ''; $queried_object = get_term_by( 'id', $preview_term_id, $preview_taxonomy ); } } elseif ( $preview_type == 'archive-cpt' ) { $preview_post_type = self::get_template_setting( 'templatePreviewPostType', $post_id ); $queried_object = get_post_type_object( $preview_post_type ); } elseif ( $preview_type == 'archive-author' ) { $template_preview_author = self::get_template_setting( 'templatePreviewAuthor', $post_id ); $queried_object = get_user_by( 'id', $template_preview_author ); } } // It is an ajax call but it is not inside a template // elseif ( bricks_is_ajax_call() && isset( $_POST['action'] ) && strpos( $_POST['action'], 'bricks_' ) === 0 ) { // Use is_bricks_preview() to apply for Api::render_element() too (@since 1.12.2) elseif ( self::is_bricks_preview() ) { $queried_object = get_post( $post_id ); } // In a query loop elseif ( $looping_query_id ) { $queried_object = Query::get_loop_object( $looping_query_id ); } if ( empty( $queried_object ) ) { $queried_object = get_queried_object(); } return $queried_object; } /** * Calculate the excerpt of a post (product, or any other cpt) * * @param WP_Post $post * @param int $excerpt_length * @param string $excerpt_more * @param boolean $keep_html */ public static function get_the_excerpt( $post, $excerpt_length, $excerpt_more = null, $keep_html = false ) { $post = get_post( $post ); if ( empty( $post ) ) { return ''; } if ( post_password_required( $post ) ) { return esc_html__( 'There is no excerpt because this is a protected post.', 'bricks' ); } /** * Relevanssi compatibility (the modified excerpt is stored in the global post_excerpt field) * * Bricks will not trim the Relevanssi excerpt any further. * * @since 1.9.1 */ if ( is_search() && function_exists( 'relevanssi_do_excerpt' ) ) { global $post; return $post->post_excerpt; } $text = $post->post_excerpt; // No excerpt, generate one if ( $text == '' ) { $text = get_the_content( '', false, $post ); $text = strip_shortcodes( $text ); $text = function_exists( 'excerpt_remove_blocks' ) ? excerpt_remove_blocks( $text ) : $text; // Run function_exists for ClassicPress $text = str_replace( ']]>', ']]>', $text ); } /** * Apply excerpt length filter, if default $excerpt_length of 55 words is used * * To apply correct excerpt limit length in-loop in the builder: {post_excerpt:10} * * @since 1.8.6 */ if ( $excerpt_length === 55 ) { $excerpt_length = apply_filters( 'excerpt_length', $excerpt_length ); } $excerpt_more = isset( $excerpt_more ) ? $excerpt_more : '…'; $excerpt_more = apply_filters( 'excerpt_more', $excerpt_more ); $text = self::trim_words( $text, $excerpt_length, $excerpt_more, $keep_html ); /** * Filters the trimmed excerpt string. * * @param string $text The trimmed text. * @param string $raw_excerpt The text prior to trimming. * * @since 2.8.0 */ return apply_filters( 'wp_trim_excerpt', $text, $post->post_excerpt ); } /** * Get a cloned post object without More block markers in post_content. * * @since 2.3.6 * * @param WP_Post|int $post Post object or ID. * * @return WP_Post|null */ public static function get_post_without_more_block( $post ) { $post = get_post( $post ); if ( ! $post ) { return null; } $post_without_more = clone $post; $post_without_more->post_content = self::remove_more_block_from_content( $post->post_content ); return $post_without_more; } /** * Remove Gutenberg and classic More markers from post content. * * @since 2.3.6 * * @param string $content Post content. * * @return string */ public static function remove_more_block_from_content( $content ) { if ( strpos( $content, '<!--more' ) === false && strpos( $content, 'wp:more' ) === false && strpos( $content, '<!--noteaser-->' ) === false ) { return $content; } if ( function_exists( 'parse_blocks' ) && function_exists( 'serialize_blocks' ) && has_blocks( $content ) ) { $remove_more_blocks = function( $blocks ) use ( &$remove_more_blocks ) { $filtered_blocks = []; foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) && $block['blockName'] === 'core/more' ) { continue; } if ( ! empty( $block['innerBlocks'] ) ) { $block['innerBlocks'] = $remove_more_blocks( $block['innerBlocks'] ); } $filtered_blocks[] = $block; } return $filtered_blocks; }; $content = serialize_blocks( $remove_more_blocks( parse_blocks( $content ) ) ); } $content = preg_replace( '/<!--\s*\/?wp:more\b[^>]*-->/', '', $content ); $content = preg_replace( '/<!--more(.*?)?-->/', '', $content ); $content = str_replace( '<!--noteaser-->', '', $content ); return $content; } /** * Trim a text string to a certain number of words. * * @since 1.6.2 * * @param string $text * @param int $length * @param string $more * @param boolean $keep_html */ public static function trim_words( $text = '', $length = 15, $more = null, $keep_html = false, $wpautop = true ) { if ( $text === '' ) { return ''; } $more = isset( $more ) ? $more : '…'; /** * Ensure length is an integer and not larger than PHP_INT_MAX or it will be converted to float * * @since 1.12 */ $length = (int) $length; if ( defined( 'PHP_INT_MAX' ) && $length >= PHP_INT_MAX ) { $length = PHP_INT_MAX - 1; } /** * Strip all HTML tags (wp_trim_words) * * We also need the ability to keep them. * Example: {woo_product_excerpt}. * * Refers to: https://stackoverflow.com/questions/36078264/i-want-to-allow-html-tag-when-use-the-wp-trim-words * * @since 1.6 */ if ( $keep_html ) { // False for Rich & basic text element (@since 1.9.8) if ( $wpautop ) { $text = wpautop( $text ); } $text = force_balance_tags( html_entity_decode( wp_trim_words( htmlentities( $text ), $length, $more ) ) ); } else { $text = wp_trim_words( $text, $length, $more ); } return $text; } /** * Posts navigation * * @return string * * @since 1.0 */ public static function posts_navigation( $current_page, $total_pages ) { $posts_navigation_html = '<div class="bricks-pagination" role="navigation" aria-label="' . esc_attr__( 'Pagination', 'bricks' ) . '">'; if ( $total_pages < 2 ) { return $posts_navigation_html . '</div>'; } $args = [ 'type' => 'list', 'current' => $current_page, 'total' => $total_pages, 'prev_text' => '←', 'next_text' => '→', ]; // NOTE: Undocumented $args = apply_filters( 'bricks/paginate_links_args', $args ); $pagination_links = paginate_links( $args ); // Adding 'aria-label' attributes to previous & next links (@since 1.9) if ( $pagination_links ) { $pagination_links = str_replace( '<a class="prev page-numbers"', '<a class="prev page-numbers" aria-label="' . esc_attr__( 'Previous page', 'bricks' ) . '"', $pagination_links ); $pagination_links = str_replace( '<a class="next page-numbers"', '<a class="next page-numbers" aria-label="' . esc_attr__( 'Next page', 'bricks' ) . '"', $pagination_links ); } $posts_navigation_html .= $pagination_links; $posts_navigation_html .= '</div>'; return $posts_navigation_html; } /** * Pagination within post * * To add ul > li structure as 'link_before' & 'link_after' are not working. * * @since 1.8 */ public static function page_break_navigation() { $pagination_html = wp_link_pages( [ 'before' => '<div class="bricks-pagination"><ul><span class="title">' . esc_html__( 'Pages:', 'bricks' ) . '</span>', 'after' => '</ul></div>', 'echo' => false, ] ); // Wrap each <a> in a <li> $pagination_html = str_replace( '<a', '<li><a', $pagination_html ); $pagination_html = str_replace( '</a>', '</a></li>', $pagination_html ); // Wrap each <span> (current page) in a <li> $pagination_html = str_replace( '<span', '<li><span', $pagination_html ); $pagination_html = str_replace( '</span>', '</span></li>', $pagination_html ); return $pagination_html; } /** Get global class data by ID * * @param string $class_id * @return array|null * * @since 2.0.2 */ public static function get_global_class_by_id( $class_id, $key ) { // Get global classes by ID if ( empty( Database::$global_data['globalClassesById'] ) ) { $global_classes = Database::$global_data['globalClasses'] ?? []; Database::$global_data['globalClassesById'] = []; foreach ( $global_classes as $class ) { if ( isset( $class['id'] ) ) { Database::$global_data['globalClassesById'][ $class['id'] ] = $class; } } } $global_class = Database::$global_data['globalClassesById'][ $class_id ] ?? null; return $key ? $global_class[ $key ] ?? null : $global_class; } /** * Element placeholder HTML * * @since 1.0 */ public static function get_element_placeholder( $data = [], $type = 'info' ) { // Placeholder style for Shortcode element 'showPlaceholder' (@since 1.7.2) $styles = $data['style'] ?? ''; $style = ''; if ( is_array( $styles ) ) { foreach ( $styles as $css_property => $css_value ) { if ( $css_value !== '' ) { // Value is number: Add defaultUnit 'px' to the end if ( is_numeric( $css_value ) ) { $css_value .= 'px'; } // Add !important to supercede other !important styles (#86bycnj5j) $style .= "$css_property: $css_value !important;"; } } } // Add support for custom class (@since 1.12.2) $classes = [ 'bricks-element-placeholder' ]; if ( ! empty( $data['class'] ) ) { $classes[] = esc_attr( $data['class'] ); } $classes = implode( ' ', $classes ); if ( $style ) { $output = '<div class="' . $classes . '" data-type="' . esc_attr( $type ) . '" style="' . esc_attr( $style ) . '">'; } else { $output = '<div class="' . $classes . '" data-type="' . esc_attr( $type ) . '">'; } if ( ! empty( $data['icon-class'] ) ) { $output .= '<i class="' . esc_attr( $data['icon-class'] ) . '"></i>'; } $output .= '<div class="placeholder-inner">'; if ( ! empty( $data['title'] ) ) { $output .= '<div class="placeholder-title">' . $data['title'] . '</div>'; } if ( ! empty( $data['description'] ) ) { $output .= '<div class="placeholder-description">' . $data['description'] . '</div>'; } $output .= '</div>'; $output .= '</div>'; return $output; } /** * Retrieves the element, the complete set of elements and the template/page ID where element belongs to * * NOTE: This function does not check for global element settings. * * @since 1.5 */ public static function get_element_data( $post_id, $element_id ) { // $post_id can be zero if home page is set to latest posts if ( empty( $element_id ) ) { return false; } // Ensure the ID is a string as it could be 6 digit number (@since 1.12) $element_id = (string) $element_id; $output = [ 'element' => [], // The element we want to find 'elements' => [], // The complete set of elements where the element is included 'source_id' => 0 // The post_id of the page or template where the element was set ]; // Get page_data via passed post_id if ( bricks_is_ajax_call() || bricks_is_rest_call() ) { Database::set_active_templates( $post_id ); } /** * If element_id contains dashes, MAYBE it is an element inside a component instance. * - User might pass in unkown element ID with dashes (eg: brxe-h6j7k8) via {query_results_count:brxe-h6j7k8} (#86c4y35pt) * * @since 2.0 */ if ( strpos( $element_id, '-' ) !== false ) { /** * Extract the string, treat first part as element ID and second part as instance ID * Example: q1w2e3r4-h6j7k8 => element ID: element, instance ID: */ $temp_parts = explode( '-', $element_id ); $temp_element_id = $temp_parts[0] ?? ''; $temp_instance_id = $temp_parts[1] ?? ''; $instance_element = self::get_element_data( $post_id, $temp_instance_id ); // STEP: Verify if instance ID is valid if ( $instance_element && isset( $instance_element['element'] ) && is_array( $instance_element['element'] ) ) { $component_instance = self::get_component_instance( $instance_element['element'] ); // STEP: Verify if component instance is valid if ( $component_instance && isset( $component_instance['elements'] ) && is_array( $component_instance['elements'] ) ) { /** * Confirmed the $element_id is an element inside a component instance * Find the query element from the component instance */ $query_element = array_filter( $component_instance['elements'], function( $element ) use ( $temp_element_id ) { return $element['id'] === $temp_element_id; } ); $query_element = reset( $query_element ); // Return: Query element not found if ( ! $query_element ) { return false; } // Set the element ID to include the instance ID $query_element['id'] = "{$temp_element_id}-{$temp_instance_id}"; $output['element'] = $query_element; $output['elements'] = $component_instance['elements'] ?? []; $output['source_id'] = 'component'; return $output; } } } $templates = []; $areas = [ 'content', 'header', 'footer' ]; foreach ( $areas as $area ) { $elements = Database::get_data( Database::$active_templates[ $area ], $area ); if ( ! empty( $elements ) && is_array( $elements ) ) { foreach ( $elements as $element ) { if ( $element['id'] === $element_id ) { $output = [ 'element' => $element, 'elements' => $elements, 'source_id' => Database::$active_templates[ $area ] ]; break ( 2 ); } // STEP: Collect possible template IDs from elements if ( $element['name'] === 'template' && ! empty( $element['settings']['template'] ) ) { $templates[] = $element['settings']['template']; } if ( $element['name'] === 'post-content' && ! empty( $element['settings']['dataSource'] ) && $element['settings']['dataSource'] == 'bricks' ) { $templates[] = $post_id; } /** * To collect template/post IDs from custom element's settings * Will retrieve the element data from the template/post and search for the target element if it is not found in the current element set. * * @return integer (post ID) * * @see https://academy.bricksbuilder.io/article/filter-bricks-get_element_data/maybe_from_post_id/ * * @since 1.11 */ $maybe_from_post_id = absint( apply_filters( 'bricks/get_element_data/maybe_from_post_id', false, $element ) ); if ( $maybe_from_post_id > 0 ) { $templates[] = $maybe_from_post_id; } } } } /** * Element not found: Try the current post * * Example: Post content element with Source "Bricks", which is not part of the template data. * * @since 1.9.9 */ if ( empty( $output['element'] ) ) { // Collect possible current post IDs $current_post_ids = array_filter( [ $post_id ?? null, Database::$page_data['preview_or_post_id'] ?? null, Query::is_looping() && Query::get_loop_object_type() === 'post' ? Query::get_loop_object_id() : null ] ); // Remove all null values and ensure unique IDs $current_post_ids = array_unique( $current_post_ids ); foreach ( $current_post_ids as $post_id ) { $elements = get_post_meta( $post_id, BRICKS_DB_PAGE_CONTENT, true ); if ( ! empty( $elements ) && is_array( $elements ) ) { foreach ( $elements as $element ) { if ( $element['id'] === $element_id ) { $output = [ 'element' => $element, 'elements' => $elements, 'source_id' => $post_id, ]; break ( 2 ); } } } } } // Not found yet? if ( empty( $output['element'] ) ) { // If we are still here, try to run through the found templates first, and remaining templates later $all_templates_query = Templates::get_templates_query( [ 'fields' => 'ids' ] ); $all_templates = ! empty( $all_templates_query->found_posts ) ? $all_templates_query->posts : []; $templates = array_merge( $templates, $all_templates ); $templates = array_unique( $templates ); foreach ( $templates as $template_id ) { $elements = get_post_meta( $template_id, BRICKS_DB_PAGE_CONTENT, true ); if ( empty( $elements ) || ! is_array( $elements ) ) { continue; } foreach ( $elements as $element ) { if ( $element['id'] === $element_id ) { $output = [ 'element' => $element, 'elements' => $elements, 'source_id' => $template_id ]; break ( 2 ); } } } } if ( empty( $output['element'] ) ) { return false; } return $output; } /** * Get element settings * * For use in AJAX functions such as form submit, pagination, etc. * * @since 1.0 * @since 1.12: Check for component instance settings */ public static function get_element_settings( $post_id = 0, $element_id = 0, $global_id = 0 ) { if ( ! $element_id ) { return false; } // Get global element settings if ( $global_id ) { $global_settings = self::get_global_element( [ 'global' => $global_id ], 'settings' ); if ( is_array( $global_settings ) ) { return $global_settings; } } // Get element $data = self::get_element_data( $post_id, $element_id ); $element = $data['element'] ?? false; // No element found if ( ! $element ) { $component_element_id = $element_id; $component_instance_id = ''; /** * Component child IDs inside rendered instances are suffixed as {elementId}-{instanceId}. */ if ( strpos( (string) $element_id, '-' ) !== false ) { $element_id_parts = explode( '-', (string) $element_id, 2 ); $component_element_id = $element_id_parts[0] ?? $element_id; $component_instance_id = $element_id_parts[1] ?? ''; } if ( $component_instance_id ) { $render_contexts = []; if ( class_exists( 'Bricks\Assets' ) && ! empty( Assets::$elements ) ) { $render_contexts[] = Assets::$elements; } if ( class_exists( 'Bricks\Frontend' ) && ! empty( Frontend::$elements ) ) { $render_contexts[] = Frontend::$elements; } /** * CSS generation can initialize query loops before Frontend::$elements is populated. * In that phase, the expanded component element is available in Assets::$elements. * * #86ca287mc; @since 2.3.7 */ foreach ( $render_contexts as $context_elements ) { foreach ( $context_elements as $context_element ) { $context_element_id = isset( $context_element['id'] ) ? (string) $context_element['id'] : ''; $context_instance_id = isset( $context_element['instanceId'] ) ? (string) $context_element['instanceId'] : ''; if ( $context_element_id === (string) $component_element_id && $context_instance_id === (string) $component_instance_id ) { $element = $context_element; break 2; } } } } // Component child: Check element in Frontend array (@since 1.12) if ( ! $element && isset( Frontend::$elements[ $element_id ] ) ) { $element = Frontend::$elements[ $element_id ]; } // Try to get the component element (@since 2.3.2) if ( ! $element ) { $component_element = self::get_component_element_by_id( $component_element_id ); if ( $component_element ) { $element = $component_element; } } if ( ! $element ) { return false; } } // Check: Component root (@since 1.12) $component_instance_settings = ! empty( $element['cid'] ) ? self::get_component_instance( $element, 'settings' ) : false; if ( $component_instance_settings ) { return $component_instance_settings; } // Get global element settings (@since 1.12.2) $global_settings = self::get_global_element( $element, 'settings' ); if ( is_array( $global_settings ) ) { return $global_settings; } // Return: element settings return $element['settings'] ?? ''; } /** * Resolve a query element ID against the current component render context. * * Dynamic tags receive the source query element ID, but component instances * store query history under "queryId-instanceId". Resolve only when the * current render context proves that the query belongs to this component * instance, preventing stale inactive-template counts from leaking in * (#86c4yu6kr). * * @param string $query_id Query element ID. * @param int $post_id Post ID. * @param array $context_element Current render element. * * @return string * @since 2.3.8 */ public static function resolve_query_element_id_for_context( $query_id, $post_id = 0, $context_element = [] ) { if ( empty( $query_id ) || $query_id === 'main' ) { return $query_id; } $query_id = (string) $query_id; // Explicit targets, including already-resolved component query IDs, must // remain untouched (#86c4yu6kr; @since 2.3.8) if ( strpos( $query_id, '-' ) !== false ) { return $query_id; } // Builder/template renders can populate query history against the preview // post ID (#86c4yu6kr; @since 2.3.8) if ( Query::is_any_looping() && isset( Database::$page_data['preview_or_post_id'] ) ) { $post_id = Database::$page_data['preview_or_post_id']; } $context_element = is_array( $context_element ) ? $context_element : []; $instance_ids = []; // Component children carry the active instance ID even though their own ID // is the source element ID (#86c4yu6kr; @since 2.3.8) if ( ! empty( $context_element['instanceId'] ) ) { $instance_ids[] = (string) $context_element['instanceId']; } // Component instance wrappers store the instance ID as the element ID and // the component ID as cid (#86c4yu6kr; @since 2.3.8) if ( ! empty( $context_element['cid'] ) && ! empty( $context_element['id'] ) ) { $instance_ids[] = (string) $context_element['id']; } $looping_query_id = Query::is_any_looping(); if ( $looping_query_id ) { $looping_element_id = Query::get_query_element_id( $looping_query_id ); // Active loops are the strongest signal when the current query already // resolved to this component instance (#86c4yu6kr; @since 2.3.8) if ( is_string( $looping_element_id ) && strpos( $looping_element_id, $query_id . '-' ) === 0 ) { return $looping_element_id; } $looping_instance_id = Query::get_query_element_instance_id( $looping_query_id ); // Nested dynamic data can execute while the component query loop is still // active (#86c4yu6kr; @since 2.3.8) if ( $looping_instance_id ) { $instance_ids[] = (string) $looping_instance_id; } } $instance_ids = array_values( array_unique( array_filter( $instance_ids ) ) ); // Validate before appending the instance ID so unsupported IDs keep their // previous behavior (#86c4yu6kr; @since 2.3.8) foreach ( $instance_ids as $instance_id ) { if ( self::is_valid_component_query_element_id( $query_id, $instance_id, $post_id, $context_element ) ) { return "{$query_id}-{$instance_id}"; } } return $query_id; } /** * Check whether a query element exists inside a component instance. * * The checks are intentionally ordered from the live query/runtime context * to stored element data. That keeps duplicated component instances accurate * while avoiding accidental matches from unrelated inactive templates * (#86c4yu6kr). * * @param string $query_id Query element ID. * @param string $instance_id Component instance ID. * @param int $post_id Post ID. * @param array $context_element Current render element. * * @return bool * @since 2.3.8 */ private static function is_valid_component_query_element_id( $query_id, $instance_id, $post_id = 0, $context_element = [] ) { $resolved_query_id = "{$query_id}-{$instance_id}"; // Query history confirms the resolved instance query has already been // registered in this render (#86c4yu6kr; @since 2.3.8) if ( Query::get_query_by_element_id( $resolved_query_id, true ) ) { return true; } // Current render stores cover builder/AJAX renders before a persistent // lookup can see the instance (#86c4yu6kr; @since 2.3.8) if ( self::render_context_has_component_query( $query_id, $instance_id ) ) { return true; } // Component instance data confirms that the source query ID exists in this // component blueprint (#86c4yu6kr; @since 2.3.8) if ( self::component_instance_has_query_element( $query_id, $instance_id, $post_id, $context_element ) ) { return true; } // Keep the legacy element lookup as the final fallback for already // materialized instance IDs (#86c4yu6kr; @since 2.3.8) $element_data = self::get_element_data( $post_id, $resolved_query_id ); return ! empty( $element_data['element']['id'] ) && (string) $element_data['element']['id'] === $resolved_query_id; } /** * Check current render element stores for a component query element. * * Assets and frontend rendering can each hold the currently materialized * component elements. Checking both keeps frontend and builder output aligned * for duplicated components (#86c4yu6kr). * * @param string $query_id Query element ID. * @param string $instance_id Component instance ID. * * @return bool * @since 2.3.8 */ private static function render_context_has_component_query( $query_id, $instance_id ) { $render_contexts = []; if ( class_exists( 'Bricks\Assets' ) && ! empty( Assets::$elements ) ) { $render_contexts[] = Assets::$elements; } if ( class_exists( 'Bricks\Frontend' ) && ! empty( Frontend::$elements ) ) { $render_contexts[] = Frontend::$elements; } foreach ( $render_contexts as $context_elements ) { foreach ( $context_elements as $context_element ) { $context_element_id = isset( $context_element['id'] ) ? (string) $context_element['id'] : ''; $context_instance_id = isset( $context_element['instanceId'] ) ? (string) $context_element['instanceId'] : ''; // Source ID plus instance metadata means this is the component-local // query element (#86c4yu6kr; @since 2.3.8) if ( $context_element_id === (string) $query_id && $context_instance_id === (string) $instance_id && self::is_query_results_element( $context_element ) ) { return true; } // Some render paths already materialize the query element ID with the // instance suffix (#86c4yu6kr; @since 2.3.8) if ( $context_element_id === "{$query_id}-{$instance_id}" && self::is_query_results_element( $context_element ) ) { return true; } } } return false; } /** * Check component instance data for a query element. * * Component blueprints keep their original source element IDs. This verifies * that the requested query ID belongs to the active component before the * caller targets "queryId-instanceId" (#86c4yu6kr). * * @param string $query_id Query element ID. * @param string $instance_id Component instance ID. * @param int $post_id Post ID. * @param array $context_element Current render element. * * @return bool * @since 2.3.8 */ private static function component_instance_has_query_element( $query_id, $instance_id, $post_id = 0, $context_element = [] ) { $instance_element = false; // The current render element may already be the component instance, which // preserves unsaved builder data (#86c4yu6kr; @since 2.3.8) if ( ! empty( $context_element['cid'] ) && ! empty( $context_element['id'] ) && (string) $context_element['id'] === (string) $instance_id ) { $instance_element = $context_element; } // Fall back to stored element data when the current render element is a // component child (#86c4yu6kr; @since 2.3.8) if ( ! $instance_element ) { $instance_data = self::get_element_data( $post_id, $instance_id ); $instance_element = $instance_data['element'] ?? false; } if ( empty( $instance_element['cid'] ) ) { return false; } $component_elements = []; if ( ! empty( $instance_element['componentInstance']['elements'] ) && is_array( $instance_element['componentInstance']['elements'] ) ) { // Builder renders can carry unsaved component data on the instance; use // it before falling back to stored component data (#86c4yu6kr; @since 2.3.8) $component_elements = $instance_element['componentInstance']['elements']; } else { $component_elements = self::get_component_instance( $instance_element, 'elements' ); } if ( ! is_array( $component_elements ) ) { return false; } foreach ( $component_elements as $component_element ) { // Match against the source query ID stored in the component blueprint, // not the instance-suffixed runtime ID (#86c4yu6kr; @since 2.3.8) if ( isset( $component_element['id'] ) && (string) $component_element['id'] === (string) $query_id && self::is_query_results_element( $component_element ) ) { return true; } } return false; } /** * Check whether an element can provide query results. * * Keep this aligned with get_query_object_from_history_or_init() so the * resolver only rewrites IDs for elements that can initialize a query * (#86c4yu6kr). * * @param array $element Element data. * * @return bool * @since 2.3.8 */ private static function is_query_results_element( $element ) { if ( ! is_array( $element ) || empty( $element['name'] ) || ! isset( $element['settings'] ) || ! is_array( $element['settings'] ) ) { return false; } $element_name = $element['name']; $settings = $element['settings']; if ( in_array( $element_name, [ 'posts', 'related-posts' ], true ) ) { return true; } if ( $element_name === 'carousel' ) { $type = $settings['type'] ?? 'media'; $items = isset( $settings['items'] ) && is_array( $settings['items'] ) ? $settings['items'] : []; // Carousel can provide query results for post slides or media slides // backed by selected/dynamic images (#86c4yu6kr; @since 2.3.8) return $type === 'posts' || ! empty( $items['useDynamicData'] ) || ! empty( $items['images'] ); } // Standard query-loop elements need both keys, matching query init logic // in get_query_object_from_history_or_init() (#86c4yu6kr; @since 2.3.8) if ( isset( $settings['query'] ) && isset( $settings['hasLoop'] ) ) { return true; } return false; } /** * Get component by 'cid' * * @param array $element * * @return boolean|array false if no component found, else return the component data. * * @since 1.12 */ public static function get_component_by_cid( $component_id ) { if ( ! $component_id ) { return false; } // Check if component exists (@since 2.1) if ( empty( Database::$global_data ) || ! is_array( Database::$global_data ) || ! isset( Database::$global_data['components'] ) || ! is_array( Database::$global_data['components'] ) ) { return false; } $components = Database::$global_data['components']; $component_index = array_search( $component_id, array_column( $components, 'id' ) ); // Return false if component not found (@since 2.1) if ( $component_index === false ) { return false; } $component = $components[ $component_index ] ?? false; if ( \Bricks\Integrations\Wpml\Wpml::is_wpml_active() && $component ) { // @since 2.1 $component = \Bricks\Integrations\Wpml\Wpml::get_translated_component( $component ); } if ( \Bricks\Integrations\Polylang\Polylang::$is_active && $component ) { // @since 2.2 $component = \Bricks\Integrations\Polylang\Polylang::get_translated_component( $component ); } return $component; } /** * Get component element by 'id' * * @param array $component_element_id * * @return boolean|array false if no component found, else return the component child element. * * @since 1.12 */ public static function get_component_element_by_id( $component_element_id ) { foreach ( Database::$global_data['components'] as $component ) { if ( ! empty( $component['elements'] ) && is_array( $component['elements'] ) ) { $component_child_index = array_search( $component_element_id, array_column( $component['elements'], 'id' ) ); // Return the component element if ( $component_child_index !== false ) { $element = $component['elements'][ $component_child_index ]; // Apply WPML translation on-the-fly if WPML is active if ( \Bricks\Integrations\Wpml\Wpml::is_wpml_active() ) { $translated_component = \Bricks\Integrations\Wpml\Wpml::get_translated_component( $component ); if ( isset( $translated_component['elements'][ $component_child_index ] ) ) { $element = $translated_component['elements'][ $component_child_index ]; } } // Apply Polylang translation on-the-fly if Polylang is active if ( \Bricks\Integrations\Polylang\Polylang::$is_active ) { $translated_component = \Bricks\Integrations\Polylang\Polylang::get_translated_component( $component ); if ( isset( $translated_component['elements'][ $component_child_index ] ) ) { $element = $translated_component['elements'][ $component_child_index ]; } } return $element; } } } } /** * Get data of specific component * * @param array $element * @param string $key (optional) * * @return boolean|array false if no component found, else return the component data. * * @since 1.12 */ public static function get_component( $element = [], $key = '' ) { $component_id = $element['cid'] ?? false; if ( ! $component_id ) { return; } $component = self::get_component_by_cid( $component_id ); return $component && $key && isset( $component[ $key ] ) ? $component[ $key ] : $component; } /** * Resolve parent property value for nested component instances. * * @param string $connection_string The parent property connection string. * @param array $request_elements Optional. Elements from current request (builder data). * @param string $parent_instance_id The parent component instance ID from render context. * * @return mixed The resolved parent property value or property default if not found. * @since 2.2 */ public static function resolve_parent_property_value( $connection_string, $request_elements = [], $parent_instance_id = '' ) { $parts = explode( ':', $connection_string ); if ( count( $parts ) < 3 || $parts[0] !== 'parent' ) { return ''; } $parent_component_id = str_replace( 'cid_', '', $parts[1] ); $parent_property_id = str_replace( 'prop_', '', $parts[2] ); // Get parent component $parent_component = self::get_component_by_cid( $parent_component_id ); if ( ! $parent_component ) { return ''; } // Find the parent property definition $parent_properties = $parent_component['properties'] ?? []; $parent_property = null; foreach ( $parent_properties as $prop ) { if ( $prop['id'] === $parent_property_id ) { $parent_property = $prop; break; } } if ( ! $parent_property ) { return ''; } // Find parent instance by matching CID and instance ID $parent_instance = null; if ( ! empty( $parent_instance_id ) ) { // Search in Assets::$elements (during CSS generation; @since 2.2) if ( class_exists( 'Bricks\Assets' ) && ! empty( \Bricks\Assets::$elements ) ) { foreach ( \Bricks\Assets::$elements as $element ) { if ( isset( $element['cid'] ) && $element['cid'] === $parent_component_id && $element['id'] === $parent_instance_id ) { $parent_instance = $element; break; } } } // Fallback: Search in Frontend::$elements (flat list of elements being rendered) if ( ! $parent_instance && class_exists( 'Bricks\Frontend' ) && ! empty( \Bricks\Frontend::$elements ) ) { foreach ( \Bricks\Frontend::$elements as $element ) { if ( isset( $element['cid'] ) && $element['cid'] === $parent_component_id && $element['id'] === $parent_instance_id ) { $parent_instance = $element; break; } } } // Fallback: Search in request_elements (builder data) if ( ! $parent_instance && ! empty( $request_elements ) && is_array( $request_elements ) ) { foreach ( $request_elements as $element ) { if ( isset( $element['cid'] ) && $element['cid'] === $parent_component_id && $element['id'] === $parent_instance_id ) { $parent_instance = $element; break; } } } // Fallback: Search in page data (frontend) if ( ! $parent_instance ) { $page_elements = Database::$page_data['elements'] ?? []; foreach ( $page_elements as $element ) { if ( isset( $element['cid'] ) && $element['cid'] === $parent_component_id && $element['id'] === $parent_instance_id ) { $parent_instance = $element; break; } } } } if ( ! $parent_instance ) { // Fallback to property default value if parent instance not found return $parent_property['default'] ?? ''; } // Get parent property value from instance or use default $parent_instance_props = $parent_instance['properties'] ?? []; $parent_value = $parent_instance_props[ $parent_property_id ] ?? ''; // Use parent property default if no instance value set if ( $parent_value === '' ) { $parent_value = $parent_property['default'] ?? ''; } return $parent_value; } /** * Return component instance * * Set root component settings and child component settings based on connected property settings. * * Use custom property settings or fallback to default property value. * * @param array $element * @param string $key settings, element (return specific component element) * * @return boolean|array false if no component found, else return the component or if $key such as 'settings' provided, the specific component element data. * * @since 1.12 */ public static function get_component_instance( $element = [], $key = '' ) { $component_id = $element['cid'] ?? false; $component = $component_id ? self::get_component_by_cid( $component_id ) : false; if ( ! $component ) { return; } $component_props = $component['properties'] ?? []; $instance_props = $element['properties'] ?? []; $instance_css_id = $element['settings']['_cssId'] ?? ''; // Get component element $component_element = self::get_component_element_by_id( $component_id ); // Use component element key-value in passed $element $key (i.e. settings) if ( $key && isset( $element[ $key ] ) && isset( $component_element[ $key ] ) ) { $element[ $key ] = $component_element[ $key ]; } // Prefer the direct slot payload ID so nested component property references stay in the slotted instance context. // (#86ca4tpmq; @since 2.3.8) $parent_instance_id = $element['slotInstanceId'] ?? $element['instanceId'] ?? $element['id']; // Loop over connected properties to populate component element settings with custom or default values foreach ( $component_props as $prop ) { $instance_value = isset( $instance_props[ $prop['id'] ] ) ? $instance_props[ $prop['id'] ] : ''; $default_value = isset( $prop['default'] ) ? $prop['default'] : ''; $connections_by_element_id = $prop['connections'] ?? []; // Resolve parent property connections for nested components (@since 2.2) if ( is_string( $instance_value ) && strpos( $instance_value, 'parent:' ) === 0 ) { $resolved_value = self::resolve_parent_property_value( $instance_value, [], $parent_instance_id ); $instance_value = $resolved_value; } foreach ( $connections_by_element_id as $element_id => $setting_keys ) { // Update component element settings foreach ( $component['elements'] as &$component_child ) { if ( $component_child['id'] == $element_id ) { // Use "==" instead of "===" as $element_id could be an integer foreach ( $setting_keys as $setting_key ) { // Handle connected controls inside query control (i.e. ignore_sticky_posts; @since 2.0) $setting_parts = explode( ':', $setting_key ); $setting_key = count( $setting_parts ) > 1 ? $setting_parts[0] : $setting_key; $sub_setting_key = $setting_parts[1] ?? null; // Is element condition setting (@since 2.0) if ( strpos( $setting_key, '_conditions' ) === 0 ) { $parts = explode( '|', $setting_key ); if ( count( $parts ) !== 3 ) { continue; } if ( ! isset( $component_child['settings']['_conditions'] ) || ! is_array( $component_child['settings']['_conditions'] ) ) { continue; } $condition_id = $parts[1]; $condition_key = $parts[2]; // Find target condition foreach ( $component_child['settings']['_conditions'] as $conditions_idx => $conditions ) { foreach ( $conditions as $cidx => $condition ) { // Cannot check if $condition_key is set as it could be empty but connected if ( isset( $condition['id'] ) && $condition['id'] === $condition_id && isset( $condition['key'] ) ) { // Use instance value if set > use default value > unset the setting if ( $instance_value !== '' ) { $component_child['settings']['_conditions'][ $conditions_idx ][ $cidx ][ $condition_key ] = $instance_value; } elseif ( $default_value !== '' ) { $component_child['settings']['_conditions'][ $conditions_idx ][ $cidx ][ $condition_key ] = $default_value; } else { // If no value is set, we set it to null, so we know that it's empty (@since 2.0) $component_child['settings']['_conditions'][ $conditions_idx ][ $cidx ][ $condition_key ] = null; } break 2; } } } continue; } // STEP: Handle settings property mapping // Subsetting key is set: Set it to the component child settings (@since 2.0) elseif ( $sub_setting_key ) { if ( $instance_value !== '' ) { if ( ! isset( $component_child['settings'][ $setting_key ] ) ) { $component_child['settings'][ $setting_key ] = []; } $component_child['settings'][ $setting_key ][ $sub_setting_key ] = $instance_value; } else { unset( $component_child['settings'][ $setting_key ][ $sub_setting_key ] ); } continue; } // Property type "toggle" set to 'off': Clear the setting (@since 2.0) elseif ( $prop['type'] === 'toggle' && $instance_value === 'off' ) { unset( $component_child['settings'][ $setting_key ] ); continue; } /** * STEP: Property type "class": Merge with or replace element classes * * Check: Not in builder to avoid adding classes on all instances (see: loadData.htmlAttributes) * * @since 2.0 */ elseif ( $prop['type'] === 'class' && ! bricks_is_builder() ) { $property_global_class_ids = ! empty( $instance_value ) ? $instance_value : $default_value; if ( $property_global_class_ids ) { if ( ! is_array( $property_global_class_ids ) ) { // Move single global class ID into an array $property_global_class_ids = [ $property_global_class_ids ]; } // Custom options: Get global class IDs from the property option.value if ( ! empty( $prop['options'] ) ) { $property_option_ids = $property_global_class_ids; foreach ( $property_option_ids as $property_option_id ) { $option_index = array_search( $property_option_id, array_column( $prop['options'], 'id' ) ); $option_class_ids = $prop['options'][ $option_index ]['value'] ?? false; if ( is_array( $option_class_ids ) && ! empty( $option_class_ids ) ) { $property_global_class_ids = array_merge( $property_global_class_ids, $option_class_ids ); } } } // Add property instance class IDs to self::$global_classes_elements[ $css_class_id ][] = $element['name']; /** * NOTE: Outcomment this as it's creating unrelated style * * The global class ids is not connected to this $element * * @see #86c4957mc */ // foreach ( $property_global_class_ids as $property_global_class_id ) { // Assets::$global_classes_elements[ $property_global_class_id ][] = $element['name']; // } // Check: Replace global classes set on the element if ( isset( $prop['replace'] ) ) { $component_child['settings']['_cssGlobalClassesPropsReplace'] = true; } // Merge global classes of this property with existing property global classes $component_child['settings']['_cssGlobalClassesProps'] = array_merge( $component_child['settings']['_cssGlobalClassesProps'] ?? [], $property_global_class_ids ); } } // Use instance value if set elseif ( $instance_value !== '' ) { $component_child['settings'][ $setting_key ] = $instance_value; } // Use default value elseif ( $default_value !== '' ) { $component_child['settings'][ $setting_key ] = $default_value; } // No value set on the instance, nor as a property default: Set it to null, so we know that it's empty (@since 2.0) else { $component_child['settings'][ $setting_key ] = null; } } } // Update key-value with update property values if ( $key && isset( $element[ $key ] ) && ( $element['id'] === $component_child['id'] || $element['cid'] === $component_child['id'] ) ) { $element[ $key ] = $component_child[ $key ]; } } } } // Loop over all component elements to merge or replace global classes (@since 2.0) if ( ! empty( $component['elements'] ) && is_array( $component['elements'] ) ) { foreach ( $component['elements'] as &$component_child ) { $global_class_ids = $component_child['settings']['_cssGlobalClassesProps'] ?? []; // Set instance CSS ID (@since 2.1) if ( $instance_css_id && $component_id === $component_child['id'] ) { $component_child['settings']['_cssId'] = $instance_css_id; } // Without "Multiple options" enabled, global class ID is stored as a string if ( is_string( $global_class_ids ) ) { $global_class_ids = explode( ' ', $global_class_ids ); } if ( is_array( $global_class_ids ) && ! empty( $global_class_ids ) ) { // Replace global classes set on the element if ( isset( $component_child['settings']['_cssGlobalClassesPropsReplace'] ) ) { $component_child['settings']['_cssGlobalClasses'] = $global_class_ids; } // Merge global classes of this property with existing element global classes else { $current_global_class_ids = $component_child['settings']['_cssGlobalClasses'] ?? []; if ( is_string( $current_global_class_ids ) ) { // Ensure string is converted to array (#86c85hk86; @since 2.3) $current_global_class_ids = explode( ' ', $current_global_class_ids ); } $component_child['settings']['_cssGlobalClasses'] = array_unique( array_merge( $current_global_class_ids, $global_class_ids ) ); } unset( $component_child['settings']['_cssGlobalClassesProps'] ); unset( $component_child['settings']['_cssGlobalClassesPropsReplace'] ); } } } // Return specific element if ( $key === 'element' ) { return $component_element; } // Return value of passed $element $key if ( $key && isset( $element[ $key ] ) ) { return $element[ $key ]; } // Return component or specific key-value like 'elements' return $key && isset( $component[ $key ] ) ? $component[ $key ] : $component; } /** * Get component slot children, including nested descendants. * * Component instance slot content is stored in the page element tree. Partial renders need * the whole slot subtree so nested local children remain renderable by original ID. * (#86c7p02wq; @since 2.3.8) * * @param array $element Component instance element. * @param array $elements Source elements. * * @return array * * @since 2.3.8 */ public static function get_component_slot_child_elements( $element = [], $elements = [] ) { if ( empty( $element['slotChildren'] ) || ! is_array( $element['slotChildren'] ) || empty( $elements ) || ! is_array( $elements ) ) { return []; } $indexed_elements = []; $indexed_elements_by_context = []; $indexed_elements_by_key = []; foreach ( $elements as $source_key => $source_element ) { if ( ! is_array( $source_element ) || empty( $source_element['id'] ) ) { continue; } $source_element_id = (string) $source_element['id']; $source_lookup_key = is_string( $source_key ) ? (string) $source_key : $source_element_id; // AJAX render paths may pass elements already keyed by their contextual slot lookup ID. $indexed_elements_by_key[ $source_lookup_key ] = [ 'element' => $source_element, 'key' => $source_lookup_key, ]; if ( ! isset( $indexed_elements[ $source_element_id ] ) ) { $indexed_elements[ $source_element_id ] = [ 'element' => $source_element, 'key' => $source_lookup_key, ]; } $source_instance_id = $source_element['slotInstanceId'] ?? $source_element['instanceId'] ?? ''; if ( $source_instance_id ) { // Keep per-instance IDs separate so nested slots can contain repeated component child IDs. $indexed_elements_by_context[ (string) $source_instance_id ][ $source_element_id ] = [ 'element' => $source_element, 'key' => $source_lookup_key, ]; } } if ( empty( $indexed_elements ) ) { return []; } $component_id = $element['cid'] ?? $element['parentComponent'] ?? false; $instance_id = $element['slotInstanceId'] ?? $element['instanceId'] ?? $element['id'] ?? false; $collected = []; $collect_element = function( $child_id, $is_slot_root = false, $context_component_id = false, $context_instance_id = false ) use ( &$collect_element, &$collected, $indexed_elements, $indexed_elements_by_context, $indexed_elements_by_key, $component_id, $instance_id ) { $child_id = (string) $child_id; $context_component_id = $context_component_id ?: $component_id; $context_instance_id = $context_instance_id ?: $instance_id; if ( empty( $child_id ) ) { return; } $resolved_child = false; if ( $context_instance_id ) { // Prefer the generated local lookup key, then fall back to the element's instance context. $context_lookup_key = $context_instance_id . '-' . $child_id; if ( isset( $indexed_elements_by_key[ $context_lookup_key ] ) ) { $resolved_child = $indexed_elements_by_key[ $context_lookup_key ]; } elseif ( isset( $indexed_elements_by_context[ (string) $context_instance_id ][ $child_id ] ) ) { $resolved_child = $indexed_elements_by_context[ (string) $context_instance_id ][ $child_id ]; } } if ( ! $resolved_child && isset( $indexed_elements[ $child_id ] ) ) { $resolved_child = $indexed_elements[ $child_id ]; } if ( ! $resolved_child ) { return; } $collection_id = (string) $resolved_child['key']; if ( isset( $collected[ $collection_id ] ) ) { return; } $child_element = $resolved_child['element']; $is_local_component = ! empty( $child_element['cid'] ); // Keep local slot subtree child traversal on original IDs. (#86c876000; @since 2.3.8) $child_element['isSlotChild'] = true; $child_element['slotElementKey'] = $collection_id; if ( $context_component_id ) { $child_element['parentComponent'] = $context_component_id; } if ( $context_instance_id ) { $child_element['instanceId'] = $context_instance_id; } // Slot root elements are stored as children of the component instance in local data. if ( $is_slot_root && $context_instance_id ) { $child_element['parent'] = $context_instance_id; } /** * A component inserted anywhere inside a slot subtree is still a local instance. Its * query ID and own slots must resolve against this local ID, not the outer component * instance. * (#86c82k2b3; @since 2.3.8) */ if ( $is_local_component ) { $child_element['slotInstanceId'] = $child_element['id']; } $collected[ $collection_id ] = $child_element; // Crossing into a local component starts a new instance context for its // component children and any slots it owns. $next_component_id = $is_local_component ? $child_element['cid'] : $context_component_id; $next_instance_id = $is_local_component ? $child_element['id'] : $context_instance_id; if ( ! empty( $child_element['children'] ) && is_array( $child_element['children'] ) ) { foreach ( $child_element['children'] as $grand_child_id ) { $collect_element( $grand_child_id, false, $next_component_id, $next_instance_id ); } } if ( ! empty( $child_element['slotChildren'] ) && is_array( $child_element['slotChildren'] ) ) { // Nested local components can own slots too, even when the component itself // is wrapped inside another component's slot. foreach ( $child_element['slotChildren'] as $slot_child_ids ) { if ( ! is_array( $slot_child_ids ) ) { continue; } foreach ( $slot_child_ids as $nested_slot_child_id ) { $collect_element( $nested_slot_child_id, true, $next_component_id, $next_instance_id ); } } } }; foreach ( $element['slotChildren'] as $slot_child_ids ) { if ( ! is_array( $slot_child_ids ) ) { continue; } foreach ( $slot_child_ids as $slot_child_id ) { $collect_element( $slot_child_id, true ); } } return array_values( $collected ); } /** * Get all component elements recursively inside block-editor.php * Similar logic as in assets.php $process_component_elements function * * @since 2.2 (#86c7ac7wk) */ public static function get_component_elements_recursive( $component_instance, &$all_elements, $include_hidden = false ) { // Add component instance but exclude elements array to avoid duplication $component_instance_without_elements = $component_instance; unset( $component_instance_without_elements['elements'] ); $all_elements[] = $component_instance_without_elements; if ( empty( $component_instance['elements'] ) || ! is_array( $component_instance['elements'] ) ) { return; } foreach ( $component_instance['elements'] as $component_element ) { // Check if element is a nested component instance if ( ! empty( $component_element['cid'] ) ) { $nested_component_instance = self::get_component_instance( $component_element ); if ( ! empty( $nested_component_instance ) ) { // Recursively process nested component elements self::get_component_elements_recursive( $nested_component_instance, $all_elements, $include_hidden ); } } // Add non-hidden elements (or all elements if in builder) elseif ( $include_hidden || empty( $component_element['settings']['_hideElementFrontend'] ) ) { $all_elements[] = $component_element; } } } /** * Get data of specific global element * * @param array $element * * @return boolean|array false if no global element found, else return the global element data. * * @since 1.3.5 */ public static function get_global_element( $element = [], $key = '' ) { $data = false; $global_elements = is_array( Database::$global_data['elements'] ) ? Database::$global_data['elements'] : []; foreach ( $global_elements as $global_element ) { // @since 1.2.1 (check against element 'global' property) if ( ! empty( $global_element['global'] ) && ! empty( $element['global'] ) && $global_element['global'] === $element['global'] ) { $data = $key && isset( $global_element[ $key ] ) ? $global_element[ $key ] : $global_element; } // @pre 1.2.1 (check against element 'id' property) elseif ( ! empty( $global_element['id'] ) && ! empty( $element['id'] ) && $global_element['id'] === $element['id'] ) { $data = $key && isset( $global_element[ $key ] ) ? $global_element[ $key ] : $global_element; } if ( $data ) { break; } } return $data; } /** * Get posts options (max 50 results) * * @since 1.0 */ public static function get_posts_by_post_id( $query_args = [] ) { // NOTE: Undocumented $query_args = apply_filters( 'bricks/helpers/get_posts_args', $query_args ); $query_args = wp_parse_args( $query_args, [ 'post_type' => 'any', 'post_status' => 'any', 'posts_per_page' => 100, 'orderby' => 'post_type', 'order' => 'DESC', 'no_found_rows' => true ] ); // Query max. 100 posts to avoid running into any memory limits if ( $query_args['posts_per_page'] == -1 ) { $query_args['posts_per_page'] = 100; } unset( $query_args['fields'] ); // Make sure the output is standard // Don't specify meta_key to get all posts for 'templatePreviewPostId' $posts = get_posts( $query_args ); $posts_options = []; foreach ( $posts as $post ) { // Skip non-content templates (header template, footer template) if ( $post->post_type === BRICKS_DB_TEMPLATE_SLUG && Templates::get_template_type( $post->ID ) !== 'content' ) { continue; } $post_type_object = get_post_type_object( $post->post_type ); $post_title = get_the_title( $post ); $post_title .= $post_type_object ? ' (' . $post_type_object->labels->singular_name . ')' : ' (' . ucfirst( $post->post_type ) . ')'; $posts_options[ $post->ID ] = $post_title; } return $posts_options; } /** * Get a list of supported content types for template preview * * @return array */ public static function get_supported_content_types() { $types = [ 'archive-recent-posts' => esc_html__( 'Archive', 'bricks' ) . ' (' . esc_html__( 'Recent posts', 'bricks' ) . ')', 'archive-author' => esc_html__( 'Archive', 'bricks' ) . ' (' . esc_html__( 'Author', 'bricks' ) . ')', 'archive-date' => esc_html__( 'Archive', 'bricks' ) . ' (' . esc_html__( 'Date', 'bricks' ) . ')', 'archive-cpt' => esc_html__( 'Archive', 'bricks' ) . ' (' . esc_html__( 'Posts', 'bricks' ) . ')', 'archive-term' => esc_html__( 'Archive', 'bricks' ) . ' (' . esc_html__( 'Term', 'bricks' ) . ')', 'single' => esc_html__( 'Single', 'bricks' ) . ' (' . esc_html__( 'Post', 'bricks' ) . '/' . esc_html__( 'Page', 'bricks' ) . '/' . 'CPT' . ')', 'search' => esc_html__( 'Search results', 'bricks' ), ]; // NOTE: Undocumented $types = apply_filters( 'bricks/template_preview/supported_content_types', $types ); return $types; } /** * Get editor mode of requested page * * @param int $post_id * * @since 1.0 */ public static function get_editor_mode( $post_id = 0 ) { if ( ! $post_id ) { $post_id = get_the_ID(); } return get_post_meta( $post_id, BRICKS_DB_EDITOR_MODE, true ); } /** * Check if post/page/cpt renders with Bricks * * @param int $post_id / $queried_object_id The post ID. * * @return boolean */ public static function render_with_bricks( $post_id = 0 ) { // When editing with Elementor we need to tell Bricks to render templates as WordPress // @see https://elementor.com/help/the-content-area-was-not-found-error/ if ( isset( $_GET['elementor-preview'] ) ) { return false; } // NOTE: Undocumented (@since 1.5.4) $render = apply_filters( 'bricks/render_with_bricks', null, $post_id ); // Returm only if false otherwise it doesn't perform other important checks (@since 1.5.4) if ( $render === false ) { return false; } // Skip WooCommerce, if disabled on Bricks Settings in case is_shop if ( ! Woocommerce::$is_active && function_exists( 'is_shop' ) && is_shop() ) { return false; } // Check current page type $current_page_type = isset( Database::$page_data['current_page_type'] ) ? Database::$page_data['current_page_type'] : ''; /** * Password protected * * Execute post_password_required() only for posts or pages (@since 1.8.4 (#863h700vb)) * Otherwise will return incorrect results if the $post_id is a taxonomy ID, etc. * https://developer.wordpress.org/reference/functions/post_password_required/ */ if ( $current_page_type === 'post' && post_password_required( $post_id ) ) { return false; } $editor_mode = self::get_editor_mode( $post_id ); if ( $editor_mode === 'wordpress' ) { return false; } /** * Paid Memberships Pro: Restrict Bricks content (@since 1.5.4) * Only execute if current page is a post (@since 1.8.4 (#863h700vb)) * https://www.paidmembershipspro.com/hook/pmpro_has_membership_access_filter/ */ if ( $current_page_type === 'post' && function_exists( 'pmpro_has_membership_access' ) ) { $user_id = null; // Retrieve inside pmpro_has_membership_access directly $return_membership_levels = false; // Return boolean $pmpro_has_membership_access = pmpro_has_membership_access( $post_id, $user_id, $return_membership_levels ); return $pmpro_has_membership_access; } return true; } /** * Get Bricks data for requested page * * @param int $post_id The post ID. * @param string $type header, content, footer. * @param boolean $force_post_data Force checking only the specific post data without considering templates. * * @since 1.3.4 * * @return boolean|array */ public static function get_bricks_data( $post_id = 0, $type = 'content', $force_post_data = false ) { if ( ! $post_id ) { $post_id = get_the_ID(); } // Return if requested post is not rendered with Bricks if ( ! self::render_with_bricks( $post_id ) ) { return false; } $bricks_data = Database::get_template_data( $type, $force_post_data ); if ( empty( $bricks_data ) || ! is_array( $bricks_data ) ) { return false; } return $bricks_data; } public static function delete_bricks_data_by_post_id( $post_id = 0 ) { if ( ! $post_id ) { $post_id = get_the_ID(); } $edit_post_link = get_edit_post_link(); if ( ! $edit_post_link ) { return false; } // Return post edit URL: No post ID found if ( ! $post_id ) { return $edit_post_link; } return add_query_arg( [ 'bricks_delete_post_meta' => $post_id, 'bricks_notice' => 'post_meta_deleted', ], $edit_post_link ); } /** * Generate random hash * * Default: 6 characters long * * @return string * * @since 1.0 */ public static function generate_hash( $string, $length = 6 ) { // Generate SHA1 hexadecimal string (40-characters) $sha1 = sha1( $string ); $sha1_length = strlen( $sha1 ); $hash = ''; // Generate random site hash based on SHA1 string for ( $i = 0; $i < $length; $i++ ) { $hash .= $sha1[ rand( 0, $sha1_length - 1 ) ]; } // Convert site path to lowercase $hash = strtolower( $hash ); return $hash; } public static function generate_random_id( $echo = true ) { $hash = self::generate_hash( md5( uniqid( rand(), true ) ) ); if ( $echo ) { echo $hash; } return $hash; } /** * Generate new & unique IDs for Bricks elements * * @since 1.9.8 * * @param array $elements * @param array $element_names_only - For WPML to only regenerate for filter elements (@since 1.12.2) */ public static function generate_new_element_ids( $elements = [], $element_names_only = [] ) { if ( empty( $elements ) ) { return $elements; } // STEP: Parse the entire Bricks data postmeta as a string $bricks_data_string = wp_json_encode( $elements ); $ids_lookup = []; // STEP: Loop over all elements to store old and new element ID in $ids_lookup array foreach ( $elements as $element ) { // Skip elements that are not in the $element_names array if ( is_array( $element_names_only ) && ! empty( $element_names_only ) && ! in_array( $element['name'], $element_names_only, true ) ) { continue; } $old_id = $element['id'] ?? ''; $new_id = self::generate_random_id( false ); if ( $old_id && $new_id ) { $ids_lookup[ $old_id ] = $new_id; } } // STEP: Search for old element ID in the Bricks data string and replace with newly-generated element ID $new_elements_string = str_replace( array_keys( $ids_lookup ), array_values( $ids_lookup ), $bricks_data_string ); // STEP: Convert Bricks data string back to array, and use as new Bricks data $new_elements = json_decode( $new_elements_string, true ); return $new_elements; } /** * Get file contents from file system * * .svg, .json (Google fonts), etc. * * @since 1.8.1 */ public static function file_get_contents( $file_path, ...$args ) { // Return: File not found if ( ! file_exists( $file_path ) ) { return ''; } // Return: File not readable if ( ! is_readable( $file_path ) ) { return ''; } // STEP: Get file contents $file_contents = file_get_contents( $file_path, ...$args ); // Return: Empty file contents if ( empty( $file_contents ) ) { return ''; } return $file_contents; } /** * Return WP dashboard Bricks settings url * * @since 1.0 */ public static function settings_url( $params = '' ) { return admin_url( "/admin.php?page=bricks-settings$params" ); } /** * Return WP dashboard Bricks elements manager url * * @since 2.0 */ public static function elements_manager_url( $params = '' ) { return admin_url( "/admin.php?page=bricks-elements$params" ); } /** * Return Bricks Academy link * * @since 1.0 */ public static function article_link( $path, $text ) { return '<a href="https://academy.bricksbuilder.io/article/' . $path . '" target="_blank" rel="noopener">' . $text . '</a>'; } /** * Return the edit post link (ot the preview post link) * * @param int $post_id The post ID. * * @since 1.2.1 */ public static function get_preview_post_link( $post_id ) { $template_preview_post_id = self::get_template_setting( 'templatePreviewPostId', $post_id ); if ( $template_preview_post_id ) { $post_id = $template_preview_post_id; } return get_edit_post_link( $post_id ); } /** * Dev helper to var dump nicely formatted * * @since 1.0 */ public static function pre_dump( $data ) { echo '<pre>'; var_dump( $data ); echo '</pre>'; } /** * Dev helper to error log array values * * @since 1.0 */ public static function log( $data ) { error_log( print_r( $data, true ) ); } /** * Logs a message if WordPress debug is enabled. * * @param string $message The message to log. * * @since 1.10.2 (for WPML) */ public static function maybe_log( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { error_log( print_r( $message, true ) ); // phpcs:ignore } } } /** * Custom wp_remote_get function */ public static function remote_get( $url, $args = [] ) { if ( ! isset( $args['timeout'] ) ) { $args['timeout'] = 30; } // Disable to avoid Let's Encrypt SSL root certificate expiration issue if ( ! isset( $args['sslverify'] ) ) { $args['sslverify'] = false; } $args = apply_filters( 'bricks/remote_get', $args, $url ); return wp_remote_get( $url, $args ); } /** * Custom wp_remote_post function * * @since 1.3.5 */ public static function remote_post( $url, $args = [] ) { if ( ! isset( $args['timeout'] ) ) { $args['timeout'] = 30; } // Disable to avoid Let's Encrypt SSL root certificate expiration issue if ( ! isset( $args['sslverify'] ) ) { $args['sslverify'] = false; } $args = apply_filters( 'bricks/remote_post', $args, $url ); return wp_remote_post( $url, $args ); } /** * Generate swiperJS breakpoint data-options (carousel, testimonial) * * Set slides to show & scroll per breakpoint. * Swiper breakpoint values use "min-width". so descent breakpoints from largest to smallest. * * https://swiperjs.com/swiper-api#param-breakpoints * * @since 1.3.5 * * @since 1.5.1: removed old 'responsive' repeater controls due to custom breakpoints */ public static function generate_swiper_breakpoint_data_options( $settings ) { $breakpoints = []; foreach ( Breakpoints::$breakpoints as $index => $breakpoint ) { $key = $breakpoint['key']; // Swiper breakpoints are min-width based, so convert Bricks breakpoint ranges accordingly. if ( Breakpoints::$is_mobile_first ) { $min_width = $index === 0 ? 1 : intval( $breakpoint['width'] ); } else { // Get min-width value from width of next smaller breakpoint. $min_width = ! empty( Breakpoints::$breakpoints[ $index + 1 ]['width'] ) ? intval( Breakpoints::$breakpoints[ $index + 1 ]['width'] ) + 1 : 1; } // 'desktop' breakpoint (plain setting key) if ( $key === 'desktop' ) { if ( ! empty( $settings['slidesToShow'] ) ) { $breakpoints[ $min_width ]['slidesPerView'] = intval( $settings['slidesToShow'] ); } if ( ! empty( $settings['slidesToScroll'] ) ) { $breakpoints[ $min_width ]['slidesPerGroup'] = intval( $settings['slidesToScroll'] ); } } // Non-desktop breakpoint else { if ( ! empty( $settings[ "slidesToShow:{$key}" ] ) ) { $breakpoints[ $min_width ]['slidesPerView'] = intval( $settings[ "slidesToShow:{$key}" ] ); } if ( ! empty( $settings[ "slidesToScroll:{$key}" ] ) ) { $breakpoints[ $min_width ]['slidesPerGroup'] = intval( $settings[ "slidesToScroll:{$key}" ] ); } } } return $breakpoints; } /** * Generate swiperJS autoplay options (carousel, slider, testimonial) * * @since 1.5.7 */ public static function generate_swiper_autoplay_options( $settings ) { return [ 'delay' => isset( $settings['autoplaySpeed'] ) ? intval( $settings['autoplaySpeed'] ) : 3000, // Set to false if 'pauseOnHover' is true to prevent swiper stopping after first hover 'disableOnInteraction' => ! isset( $settings['pauseOnHover'] ), // Pause autoplay on mouse enter (new in v6.6: autoplay.pauseOnMouseEnter) 'pauseOnMouseEnter' => isset( $settings['pauseOnHover'] ), // Stop autoplay on last slide (@since 1.4) 'stopOnLastSlide' => isset( $settings['stopOnLastSlide'] ), ]; } /** * Verifies the integrity of the data using a hash * * @param string $hash The hash to compare against. * @param mixed $data The data to verify. * * @since 1.9.7 * * @return bool True if the data is valid, false otherwise. */ public static function verify_code_signature( $hash, $data ) { // Compute the hash of the data $computed_hash = wp_hash( $data ); // Compare the computed hash with the provided hash return $computed_hash === $hash; } /** * Code element settings: code, + executeCode * Query Loop settings: useQueryEditor + queryEditor */ public static function sanitize_element_php_code( $post_id, $element_id, $code, $signature ) { // Return no code: Code execution not enabled (Bricks setting or filter) if ( ! self::code_execution_enabled() ) { return [ 'error' => esc_html__( 'Code execution is disabled', 'bricks' ) ]; } // Filter $code content to prevent dangerous calls $disallow_keywords = apply_filters( 'bricks/code/disallow_keywords', [] ); // Check if code contains any disallowed keywords if ( is_array( $disallow_keywords ) && ! empty( $disallow_keywords ) ) { foreach ( $disallow_keywords as $keyword ) { if ( stripos( $code, $keyword ) !== false ) { // Return error: Disallowed keyword found return [ 'error' => esc_html__( 'Disallowed keyword found', 'bricks' ) . " ($keyword)" ]; } } } // Return error: Code signature verification failed if ( ! self::verify_code_signature( $signature, $code ) ) { // Return error: Nor signature found if ( ! $signature ) { return [ 'error' => esc_html__( 'No signature', 'bricks' ) ]; } else { return [ 'error' => esc_html__( 'Invalid signature', 'bricks' ) ]; } } // Return verified code in builder (current user has full access & execute_code capability) if ( Capabilities::$full_access && Capabilities::$execute_code && bricks_is_builder_call() ) { return $code; } // Clear code to populate with setting from database $code = ''; /** * STEP: Get element settings from database * * Checks also for: * - Component * - Global element */ $global_id = $element_id; $element_settings = self::get_element_settings( $post_id, $element_id, $global_id ); // STEP: Get code element setting from database if ( isset( $element_settings['code'] ) ) { $code = $element_settings['code']; $signature = $element_settings['signature'] ?? false; } // STEP: Get query editor setting from database elseif ( isset( $element_settings['query']['queryEditor'] ) ) { $code = $element_settings['query']['queryEditor']; $signature = $element_settings['query']['signature'] ?? false; } // STEP: Get global query (@since 2.1) elseif ( ! empty( $element_settings['query']['id'] ) ) { $global_query_id = $element_settings['query']['id']; foreach ( Database::$global_data['globalQueries'] as $global_query ) { if ( isset( $global_query['id'] ) && $global_query['id'] === $global_query_id ) { $code = $global_query['settings']['queryEditor'] ?? ''; $signature = $global_query['settings']['signature'] ?? false; break; } } } // Return error: Code signature verification from database failed if ( ! self::verify_code_signature( $signature, $code ) ) { // Return error: No signature found if ( ! $signature ) { return [ 'error' => esc_html__( 'No signature', 'bricks' ) ]; } else { return [ 'error' => esc_html__( 'Invalid signature', 'bricks' ) ]; } } // Return: Code safely retrieved from database return $code; } /** * Check if code execution is enabled * * Via filter or Bricks setting. * * @since 1.9.7: Code execution is disabled by default. * * @return boolean */ public static function code_execution_enabled() { $filter_result = apply_filters( 'bricks/code/disable_execution', false ); // Filter explicitly returns true: Disable code execution if ( $filter_result === true ) { return false; } // Deprecated since 1.9.7, but kept for those already using it. Only evaluate when set to false. if ( apply_filters( 'bricks/code/allow_execution', null ) === false ) { return false; } // Check the database setting if code execution is enabled return Database::get_setting( 'executeCodeEnabled', false ); } /** * Sanitize value */ public static function sanitize_value( $value ) { // URL value if ( filter_var( $value, FILTER_VALIDATE_URL ) ) { return esc_url_raw( $value ); } // Email value if ( is_email( $value ) ) { return sanitize_email( $value ); } // Text value return sanitize_text_field( $value ); } /** * Sanitize Bricks data * * During template import, etc. * * @since 1.3.7 */ public static function sanitize_bricks_data( $elements ) { if ( is_array( $elements ) ) { foreach ( $elements as $index => $element ) { // Code element: Unset "Execute Code" setting to prevent executing potentially malicious code if ( isset( $element['settings']['executeCode'] ) ) { unset( $elements[ $index ]['settings']['executeCode'] ); } // Query editor: Unset "query.useQueryEditor" setting to prevent executing potentially malicious code if ( isset( $element['settings']['query']['useQueryEditor'] ) ) { unset( $elements[ $index ]['settings']['useQueryEditor'] ); } // Query editor: Unset "query.queryEditor" setting to prevent executing potentially malicious code if ( isset( $element['settings']['query']['queryEditor'] ) ) { unset( $elements[ $index ]['settings']['queryEditor'] ); } } } return $elements; } /** * Set is_frontend = false to a element * * Use: $elements = array_map( 'Bricks\Helpers::set_is_frontend_to_false', $elements ); * * @since 1.4 */ public static function set_is_frontend_to_false( $element ) { $element['is_frontend'] = false; return $element; } /** * Get post IDs of all Bricks-enabled post types * * @see admin.php get_converter_items() * @see files.php get_css_files_list() * * @param array $custom_args Custom get_posts() arguments (@since 1.8; @see get_css_files_list). * * @since 1.4 */ public static function get_all_bricks_post_ids( $custom_args = [] ) { $args = array_merge( [ 'post_type' => array_keys( self::get_supported_post_types() ), 'posts_per_page' => -1, 'post_status' => 'any', 'fields' => 'ids', 'no_found_rows' => true, 'update_post_term_cache' => false, 'meta_query' => [ [ 'key' => BRICKS_DB_PAGE_CONTENT, 'value' => '', 'compare' => '!=', ], ], ], $custom_args ); return get_posts( $args ); } /** * Search & replace: Works for strings & arrays * * @param string $search The value being searched for. * @param string $replace The replacement value that replaces found search values. * @param string $data The string or array being searched and replaced on, otherwise known as the haystack. * * @see templates.php import_template() * * @since 1.4 */ public static function search_replace( $search, $replace, $data ) { $is_array = is_array( $data ); // Stringify array if ( $is_array ) { $data = wp_json_encode( $data ); } // Replace string $data = str_replace( $search, $replace, $data ); // Convert back to array if ( $is_array ) { $data = json_decode( $data, true ); } return $data; } /** * Google fonts are disabled (via filter OR Bricks setting) * * @see https://academy.bricksbuilder.io/article/filter-bricks-assets-load_webfonts * * @since 1.4 */ public static function google_fonts_disabled() { return ! apply_filters( 'bricks/assets/load_webfonts', true ) || isset( Database::$global_settings['disableGoogleFonts'] ); } /** * Sort variable Google Font axis (all lowercase before all uppercase) * * https://developers.google.com/fonts/docs/css2#strictness * * @since 1.8 */ public static function google_fonts_get_axis_rank( $axis ) { // lowercase axis first if ( $axis === strtolower( (string) $axis ) ) { return 0; } // uppercase axis second return 1; } /** * Stringify HTML attributes * * @param array $attributes key = attribute key; value = attribute value (string|array). * * @see bricks/header/attributes * @see bricks/footer/attributes * @see bricks/popup/attributes * * @return string * * @since 1.5 */ public static function stringify_html_attributes( $attributes ) { $strings = []; foreach ( $attributes as $key => $value ) { // Skip invalid HTML attribute name if ( ! self::is_valid_html_attribute_name( $key ) ) { continue; } // Array: 'class', etc. if ( is_array( $value ) ) { $value = join( ' ', $value ); } // To escape json strings (@since 1.6) $value = esc_attr( $value ); $strings[] = "{$key}=\"$value\""; } return join( ' ', $strings ); } /** * Validate HTML attributes * * NOTE: Not in use yet to prevent breaking changes of unintended removal of attributes. * * @since 1.10.2 */ public static function is_valid_html_attribute_name( $attribute_name ) { return true; // return preg_match( '/^[\p{L}_:][\p{L}0-9_:.\\-]*$/u', $attribute_name ) === 1; } /** * Return element attribute 'id' * * @since 1.5.1 * * @since 1.7.1: Parse dynamic data for _cssId (same for _cssClasses) */ public static function get_element_attribute_id( $id, $settings ) { $attribute_id = "brxe-{$id}"; if ( ! empty( $settings['_cssId'] ) ) { $attribute_id = bricks_render_dynamic_data( $settings['_cssId'] ); } return esc_attr( $attribute_id ); } /** * Based on the current user capabilities, check if the new elements could be changed on save (AJAX::save_post()) * * If user can only edit the content: * - Check if the number of elements is the same * - Check if the new element already existed before * * If user cannot execute code: * - Replace any code element (with execution enabled) by the saved element, * - or disable the execution (in case the element is new) * * @since 1.5.4 * * @param array $new_elements Array of elements. * @param int $post_id The post ID. * @param string $area 'header', 'content', 'footer'. * * @return array Array of elements */ public static function security_check_elements_before_save( $new_elements, $post_id, $area ) { $user_can_execute_code = Capabilities::current_user_can_execute_code(); // Return elements (user can add/remove elements & execute code permission) if ( Builder_Permissions::user_can_modify_element_count() && $user_can_execute_code ) { return $new_elements; } // Get old data structure from the database if ( $area === 'global' ) { $old_elements = get_option( BRICKS_DB_GLOBAL_ELEMENTS, [] ); } else { $area_key = Database::get_bricks_data_key( $area ); $old_elements = get_post_meta( $post_id, $area_key, true ); // wp_slash the postmeta value as update_post_meta removes backslashes via wp_unslash (@since 1.9.7) if ( is_array( $old_elements ) ) { $old_elements = wp_slash( $old_elements ); } } // Initial orphaned elements scan $new_elements = is_array( $new_elements ) ? $new_elements : []; $old_elements = is_array( $old_elements ) ? $old_elements : []; // STEP: Return old elements: User is not allowed to edit the structure, but the number of new elements differs from old structure if ( ! Builder_Permissions::user_can_modify_element_count() && count( $new_elements ) !== count( $old_elements ) ) { return $old_elements; } /** * Current user has no code execution (Bricks) capability nor 'unfiltered_html' (WordPress) capability * * Run all non-code elements through wp_filter_post_kses as WordPress does. * * @since 1.9.8 */ if ( ! $user_can_execute_code && ! current_user_can( 'unfiltered_html' ) ) { foreach ( $new_elements as $i => $new_element ) { if ( $new_element['name'] !== 'code' ) { // Use array_walk_recursive to apply wp_kses to all element settings (@since 1.11) array_walk_recursive( $new_element, [ 'Bricks\Helpers', 'apply_wp_filter_post_kses_to_array' ] ); } if ( isset( $new_element['id'] ) ) { $new_elements[ $i ] = $new_element; } } } $old_elements_indexed = []; // Index the old elements for faster check foreach ( $old_elements as $element ) { $old_elements_indexed[ $element['id'] ] = $element; } foreach ( $new_elements as $index => $element ) { // STEP: Orphaned elements scan: New elements found despite the user can only edit existing content: Remove element if ( ! Builder_Permissions::user_can_modify_element_count() && ! isset( $old_elements_indexed[ $element['id'] ] ) ) { unset( $new_elements[ $index ] ); continue; } // STEP: Code element: User doesn't have permission and code execution is enabled on element if ( ! $user_can_execute_code && $element['name'] === 'code' && isset( $element['settings']['executeCode'] ) ) { // Replace new element with old element (if it exists) if ( isset( $old_elements_indexed[ $element['id'] ] ) ) { $new_elements[ $index ] = $old_elements_indexed[ $element['id'] ]; } // Disable execution mode else { unset( $new_elements[ $index ]['settings']['executeCode'] ); } } // STEP: SVG element: User doesn't have permission and SVG has 'code' if ( ! $user_can_execute_code && $element['name'] === 'svg' && isset( $element['settings']['code'] ) ) { // Replace new element with old element (if it exists) if ( isset( $old_elements_indexed[ $element['id'] ] ) ) { $new_elements[ $index ] = $old_elements_indexed[ $element['id'] ]; } // Remove code else { unset( $new_elements[ $index ]['settings']['code'] ); } } // STEP: 'query' setting: User doesn't have permission and 'useQueryEditor' is enabled on element, plus 'queryEditor' (PHP code) exists if ( ! $user_can_execute_code && ( isset( $element['settings']['query']['useQueryEditor'] ) || isset( $element['settings']['query']['queryEditor'] ) ) ) { // Replace new element with old element (if it exists) if ( isset( $old_elements_indexed[ $element['id'] ] ) ) { $new_elements[ $index ] = $old_elements_indexed[ $element['id'] ]; } // Remove code else { if ( isset( $new_elements[ $index ]['settings']['query']['useQueryEditor'] ) ) { unset( $new_elements[ $index ]['settings']['query']['useQueryEditor'] ); } if ( isset( $new_elements[ $index ]['settings']['query']['queryEditor'] ) ) { unset( $new_elements[ $index ]['settings']['query']['queryEditor'] ); } } } // STEP: Modified 'echo' DD tag found for user without code execution capabilities if ( ! $user_can_execute_code ) { // Extract all DD echo tags from new data $new_element_string = wp_json_encode( $element ); preg_match_all( '/\{echo:([^\}]+)\}/', $new_element_string, $new_matches ); $new_matches = isset( $new_matches[1] ) && is_array( $new_matches[1] ) ? $new_matches[1] : []; // Remove all 'echo' DD tags if ( $new_matches ) { // Extract all DD echo tags from old data $old_element = $old_elements_indexed[ $element['id'] ]; $old_element_string = wp_json_encode( $old_element ); preg_match_all( '/\{echo:([^\}]+)\}/', $old_element_string, $old_matches ); $old_matches = isset( $old_matches[1] ) && is_array( $old_matches[1] ) ? $old_matches[1] : []; foreach ( $new_matches as $tag_index => $new_tag ) { $old_tag = $old_matches[ $tag_index ] ?? ''; // New tag doesn't match old tag: Replace new tag with old tag if ( $old_tag ) { $new_elements[ $index ] = json_decode( str_replace( "{echo:$new_tag}", "{echo:$old_tag}", $new_element_string ), true ); } // No old tag found: Remove new tag else { $new_elements[ $index ] = json_decode( str_replace( "{echo:$new_tag}", '', $new_element_string ), true ); } } } } } // Reindex array $new_elements = array_values( $new_elements ); // Apply filter to new elements (@since 1.12.3) $new_elements = apply_filters( 'bricks/security_check_before_save/new_elements', $new_elements, $old_elements_indexed ); return $new_elements; } /** * Parse CSS & return empty string if checks are not fulfilled * * @since 1.6.2 */ public static function parse_css( $css ) { if ( ! $css ) { return $css; } // CSS syntax error: Number of opening & closing tags differs if ( substr_count( $css, '{' ) !== substr_count( $css, '}' ) ) { return ''; } return $css; } /** * Save global classes in options table * * Skip saving empty global classes array. * * Triggered in: * * ajax.php: wp_ajax_bricks_save_post (save post in builder) * templates.php: wp_ajax_bricks_import_template (template import) * converter.php: wp_ajax_bricks_run_converter (run converter from Bricks settings) * * @since 1.7 * * @param array $global_classes * @param string $action */ public static function save_global_classes_in_db( $global_classes ) { $response = ''; $timestamp = time(); $user_id = get_current_user_id(); // Update global classes (if not empty) // NOTE: Global empty classes array can be saved, since we store it in the trash (@since 1.11) if ( is_array( $global_classes ) ) { // Remove any empty classes, then reindex classes array (#86bxgebwg) $global_classes = array_values( array_filter( $global_classes ) ); if ( count( $global_classes ) ) { $response = update_option( BRICKS_DB_GLOBAL_CLASSES, $global_classes ); // Update global classes timestamp & user_id for builder nofifications & conflicts (@since 1.9.9) update_option( BRICKS_DB_GLOBAL_CLASSES_TIMESTAMP, $timestamp ); update_option( BRICKS_DB_GLOBAL_CLASSES_USER, $user_id ); } else { $response = delete_option( BRICKS_DB_GLOBAL_CLASSES ); } } return [ 'response' => $response, 'timestamp' => $timestamp, 'user_id' => $user_id, ]; } /** * Save global variables in options table * * @param array $global_variables * @return mixed * @since 1.9.8 */ public static function save_global_variables_in_db( $global_variables ) { $response = ''; // Update global variables (if not empty) if ( is_array( $global_variables ) && count( $global_variables ) ) { $response = update_option( BRICKS_DB_GLOBAL_VARIABLES, $global_variables ); } return $response; } /** * Parse TinyMCE editor control data * * Use instead of applying 'the_content' filter to prevent rendering third-party content in within non "Post Content" elements. * * Available as static function to use in get_dynamic_data_preview_content as well (DD tag render on canvas) * * @see accordion, alert, icon-box, slider, tabs, text * * @since 1.7 */ public static function parse_editor_content( $content = '' ) { // Return: Not a text string (e.g. ACF field type color array) if ( ! is_string( $content ) ) { return $content; } /** * Remove outermost <p> tag (from rich text element) if it contains a block-level HTML tag (like an <div>, <h2>, etc.) * * Example: <p>{acf_eysiwyg}</p>, and the ACF DD tag contains: <h2>ACF heading</h2> * Rendered as: <p></p><h2>ACF heading</h2><p></p> * Expected: <h2>ACF heading</h2> * * @since 1.7 */ if ( strpos( $content, '<p>' ) === 0 && strpos( $content, '</p>' ) !== false ) { $content = preg_replace( '/^<p>(.*)<\/p>$/is', '$1', $content ); } /** * WordPress code default-filters.php reference * * Priority: 8 * run_shortcode * autoembed * * Priority: 9 * do_blocks * * Priority: 10 * wptexturize * wpautop * shortcode_unautop * prepend_attachment * wp_filter_content_tags * wp_replace_insecure_home_url * * Priority: 11 * capital_P_dangit * do_shortcode * * Priority: 20 * convert_smilies */ // Passes any unlinked URLs that are on their own line to WP_Embed::shortcode() for potential embedding (audio, video) if ( $GLOBALS['wp_embed'] instanceof \WP_Embed ) { $content = $GLOBALS['wp_embed']->run_shortcode( $content ); // (#86c2v39x2) $content = $GLOBALS['wp_embed']->autoembed( $content ); } // Priority: 10 $content = wptexturize( $content ); $content = wpautop( $content ); $content = shortcode_unautop( $content ); // $content = prepend_attachment( $content ); // Add srcset, sizes, and loading attributes to img HTML tags; and loading attributes to iframe HTML tags $content = wp_filter_content_tags( $content ); $content = wp_replace_insecure_home_url( $content ); // Priority: 11 $content = do_shortcode( $content ); // Only convert smilies if not disabled in Bricks settings (#86c695he9) (@since 2.2) if ( ! isset( Database::$global_settings['disableEmojis'] ) ) { // Priority: 20 $content = convert_smilies( $content ); } return $content; } /** * Check if post_id is a Bricks template * * Previously used get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG * But this method might accidentally return true if $post_id is a term_id or user_id, etc. * * @since 1.8 */ public static function is_bricks_template( $post_id ) { // Check current page type $current_page_type = isset( Database::$page_data['current_page_type'] ) ? Database::$page_data['current_page_type'] : ''; // In loop: Get object type of loop if ( Query::is_any_looping() ) { $looping_query_id = Query::is_any_looping(); $looping_object_type = Query::get_loop_object_type( $looping_query_id ); $current_page_type = $looping_object_type; } return $current_page_type === 'post' && get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG; } /** * Check if current request is Bricks preview * * @since 1.9.5 */ public static function is_bricks_preview() { global $wp_query; return $wp_query->get( 'post_type' ) === BRICKS_DB_TEMPLATE_SLUG || bricks_is_builder_call(); } /** * Get a post object without losing WordPress preview autosave content. * * Gutenberg's "Preview in new tab" applies unsaved content to the main query post * via WordPress' `the_preview` filter. Calling get_post() again reloads the saved * database row, so Bricks must reuse the preview-mutated query object when it * matches the requested post. * * @since 2.3.6 (#86c9wy4u5) * * @param int $post_id Post ID. * @param \WP_Post $fallback_post Optional fallback post object. * * @return \WP_Post|null */ public static function get_post_preserving_preview( $post_id = 0, $fallback_post = null ) { if ( ! $post_id && $fallback_post instanceof \WP_Post ) { $post_id = $fallback_post->ID; } if ( ! $post_id ) { $post_id = get_the_ID(); } $post = $fallback_post instanceof \WP_Post ? $fallback_post : get_post( $post_id ); if ( ! is_preview() ) { return $post; } global $wp_query; // The main query post is the source of truth for WP preview/autosave content. if ( ! empty( $wp_query->posts[0] ) && $wp_query->posts[0] instanceof \WP_Post && (int) $wp_query->posts[0]->ID === (int) $post_id ) { return $wp_query->posts[0]; } return $post; } /** * Check if the element settings contain a specific value * * Useful if the setting has diffrent keys in different breakpoints. * * Example: 'overlay', 'overlay:mobile_portrait', 'overlay:tablet_landscape', etc. * * Usage: * Helpers::element_setting_has_value( 'overlay', $settings ); // Check if $settings contains 'overlay' setting in any breakpoint * Helpers::element_setting_has_value( 'overlay:mobile', $settings ); // Check if $settings contains 'overlay' setting in mobile breakpoint * * @since 1.8 * * @param string $key * @param array $settings * * @return bool */ public static function element_setting_has_value( $key = '', $settings = [] ) { if ( ! is_array( $settings ) || empty( $key ) ) { return false; } $has_setting = false; if ( is_array( $settings ) && count( $settings ) ) { // Search array keys for where starts with $key $setting_keys = array_filter( array_keys( $settings ), function ( $setting_key ) use ( $key ) { return strpos( $setting_key, $key ) === 0; } ); if ( count( $setting_keys ) ) { // Assume the first key is the one we're looking for $first_key = reset( $setting_keys ); // Check if the value is not empty $has_setting = ! empty( $settings[ $first_key ] ); } } return $has_setting; } /** * Check if the provided url string is the current landed page * Logic improvement for performance (@since 2.0) * - Should not set ancestor page/post as current page. The active state handled by frontend JS. * - Not used in the builder. * * @since 1.8 * * @param string $url * @return bool */ public static function maybe_set_aria_current_page( $url = '' ) { if ( empty( $url ) || self::is_bricks_preview() ) { return false; } global $wp; // Normalize the provided URL $url = trailingslashit( rawurldecode( $url ) ); // Get the current page URL $current_page_url = trailingslashit( rawurldecode( home_url( $wp->request ) ) ); $set_aria_current = false; // Check if the provided URL matches the current page URL if ( $url === $current_page_url ) { $set_aria_current = true; } else { // Legacy logic to compare URLs if ( is_front_page() ) { // Front page $front_page_id = absint( get_option( 'page_on_front' ) ); // Static page as front page if ( $front_page_id > 0 ) { $set_aria_current = '/' === $url; } // Latest posts as front page ($front_page_id === 0) // Check if homepage URL is the same as the URL we are checking after removing trailing slashes, maybe user will use '/' as well else { $set_aria_current = untrailingslashit( home_url( '/' ) ) === untrailingslashit( $url ) || '/' === $url; } } // Posts page(is_home()), Category, tag, archive etc. else { // URL starts with a slash (e.g. /category/business/): Add home URL if ( substr( $url, 0, 1 ) === '/' && substr( $url, 0, 2 ) !== '/' ) { $url = home_url( $url ); } $url = trailingslashit( $url ); $set_aria_current = strcmp( rtrim( $url ), rtrim( $current_page_url ) ) === 0; } } // Undocumented: Currently used in includes/woocommerce.php return apply_filters( 'bricks/element/maybe_set_aria_current_page', $set_aria_current, $url ); } /** * Parse textarea content to account for dynamic data usage * * Useful in 'One option / feature per line' situations * * Examples: Form element options (Checkbox, Select, Radio) or Pricing Tables (Features) * * @since 1.9 * * @param string $options * @return array */ public static function parse_textarea_options( $options ) { // Render possible dynamic data tags within the options string $options = bricks_render_dynamic_data( $options ); // Strip tags like <p> or <br> when using ACF textarea or WYSIWYG fields // @pre 1.9.2 we used strip_tags, but we don't want to remove all HTML tags. $options = preg_replace( '~</?(p|br)[^>]*>~', '', $options ); // At this point the parsed value might contain a trailing line break – remove it $options = rtrim( $options ); // Finally return an array of options return explode( "\n", $options ); } /** * Use user agent string to detect browser * * @since 1.9.2 */ public static function user_agent_to_browser( $user_agent ) { $value = ''; if ( preg_match( '/chrome/i', $user_agent ) ) { $value = 'chrome'; } elseif ( preg_match( '/firefox/i', $user_agent ) ) { $value = 'firefox'; } elseif ( preg_match( '/safari/i', $user_agent ) ) { $value = 'safari'; } elseif ( preg_match( '/edge/i', $user_agent ) ) { $value = 'edge'; } elseif ( preg_match( '/opera/i', $user_agent ) ) { $value = 'opera'; } elseif ( preg_match( '/msie/i', $user_agent ) ) { $value = 'msie'; } return $value; } /** * Use user agent string to detect operating system * * @since 1.9.2 * @since 1.10.2 Sequence matters: Start with the most specific one! */ public static function user_agent_to_os( $user_agent ) { $value = ''; if ( preg_match( '/iphone/i', $user_agent ) ) { // Mozilla/5.0 (iPhone; CPU OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1 $value = 'iphone'; } elseif ( preg_match( '/ipad/i', $user_agent ) ) { // Mozilla/5.0 (iPad; CPU OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1 $value = 'ipad'; } elseif ( preg_match( '/ipod/i', $user_agent ) ) { $value = 'ipod'; } elseif ( preg_match( '/android/i', $user_agent ) ) { // Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Mobile Safari/537.36 $value = 'android'; } elseif ( preg_match( '/blackberry/i', $user_agent ) ) { $value = 'blackberry'; } elseif ( preg_match( '/webos/i', $user_agent ) ) { $value = 'webos'; } elseif ( preg_match( '/ubuntu/i', $user_agent ) ) { // Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 $value = 'ubuntu'; } elseif ( preg_match( '/linux/i', $user_agent ) ) { // Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 $value = 'linux'; } elseif ( preg_match( '/mac/i', $user_agent ) ) { // Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0 $value = 'mac'; } elseif ( preg_match( '/win/i', $user_agent ) ) { // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 $value = 'windows'; } return $value; } /** * Get user IP address * * @since 1.9.2 */ public static function user_ip_address() { $ip_address = ''; if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) { // Cloudflare $ip_address = $_SERVER['HTTP_CF_CONNECTING_IP']; } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { // Forwarded for $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR']; } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { // Default $ip_address = $_SERVER['REMOTE_ADDR']; } // Validate IP address - compatible with both IPv4 & IPv6 $ip_address = filter_var( $ip_address, FILTER_VALIDATE_IP ); return $ip_address; } /** * Populate query_vars to be used in Bricks template preview based on "Populate content" settings * * @since 1.9.1 */ public static function get_template_preview_query_vars( $post_id ) { if ( ! $post_id ) { return []; } $query_args = []; $template_settings = self::get_template_settings( $post_id ); $template_preview_type = self::get_template_setting( 'templatePreviewType', $post_id ); // @since 1.8 - Set preview type if direct edit page or post with Bricks (#861m48kv4) if ( bricks_is_builder_call() && empty( $template_settings ) && ! self::is_bricks_template( $post_id ) ) { $template_preview_type = 'direct-edit'; } switch ( $template_preview_type ) { // Archive: Recent posts case 'archive-recent-posts': $query_args['post_type'] = 'post'; break; // Archive: author case 'archive-author': $template_preview_author = self::get_template_setting( 'templatePreviewAuthor', $post_id ); if ( $template_preview_author ) { $query_args['author'] = $template_preview_author; } break; // Author date case 'archive-date': $query_args['year'] = date( 'Y' ); break; // Archive CPT case 'archive-cpt': $template_preview_post_type = self::get_template_setting( 'templatePreviewPostType', $post_id ); if ( $template_preview_post_type ) { $query_args['post_type'] = $template_preview_post_type; } break; // Archive term case 'archive-term': $template_preview_term_id_parts = isset( $template_settings['templatePreviewTerm'] ) ? explode( '::', $template_settings['templatePreviewTerm'] ) : ''; $template_preview_taxnomy = isset( $template_preview_term_id_parts[0] ) ? $template_preview_term_id_parts[0] : ''; $template_preview_term_id = isset( $template_preview_term_id_parts[1] ) ? $template_preview_term_id_parts[1] : ''; if ( $template_preview_taxnomy && $template_preview_term_id ) { $query_args['tax_query'] = [ [ 'taxonomy' => $template_preview_taxnomy, 'terms' => $template_preview_term_id, 'field' => 'term_id', ], ]; } break; // Search case 'search': $template_preview_search_term = self::get_template_setting( 'templatePreviewSearchTerm', $post_id ); if ( $template_preview_search_term ) { $query_args['s'] = $template_preview_search_term; } break; // Single case 'direct-edit': // Editing template directly case 'single': // (template condition "Content type" = "Single post/page") $template_preview_post_id = self::get_template_setting( 'templatePreviewPostId', $post_id ); if ( $template_preview_post_id ) { $query_args['p'] = $template_preview_post_id; $query_args['post_type'] = get_post_type( $template_preview_post_id ); } break; } // NOTE: Undocumented $query_args = apply_filters( 'bricks/element/builder_setup_query', $query_args, $post_id ); return $query_args; } /** * Populate query vars if the element is not using query type controls * Currently used in related-posts element and query_results_count when targeting related-posts element * * @return array * @since 1.9.3 */ public static function populate_query_vars_for_element( $element_data, $post_id ) { $element_name = $element_data['name'] ?? ''; if ( empty( $element_name ) || is_null( $post_id ) ) { return []; } $settings = $element_data['settings'] ?? []; $args = []; if ( $element_name === 'related-posts' ) { $args = [ 'posts_per_page' => $settings['count'] ?? 3, 'post__not_in' => [ $post_id ], 'no_found_rows' => true, // No pagination 'orderby' => $settings['orderby'] ?? 'rand', 'order' => $settings['order'] ?? 'DESC', 'post_status' => 'publish', 'objectType' => 'post', // @since 1.9.3 'bricks_skip_query_vars' => true, // skip Query::prepare_query_vars_from_settings() @since 1.9.3 ]; if ( ! empty( $settings['post_type'] ) ) { $args['post_type'] = $settings['post_type']; } $taxonomies = $settings['taxonomies'] ?? []; foreach ( $taxonomies as $taxonomy ) { $terms_ids = wp_get_post_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] ); if ( ! empty( $terms_ids ) ) { $args['tax_query'][] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $terms_ids, ]; } } if ( count( $taxonomies ) > 1 && isset( $args['tax_query'] ) ) { $args['tax_query']['relation'] = 'OR'; } // NOTE: Undocumented (not ideal naming as it's not only for related-posts element) $args = apply_filters( 'bricks/related_posts/query_vars', $args, $settings ); } if ( $element_name === 'carousel' ) { $type = $settings['type'] ?? 'media'; // Only populate query vars if type is 'media' if ( $type === 'media' ) { // STEP: Media type carousel might use dynamic data, so we need to normalize the settings (logic originally inside carousel element) $carousel_class_name = Elements::$elements['carousel']['class'] ?? false; if ( $carousel_class_name && ! empty( $settings['items']['useDynamicData'] ) ) { $carousel = new $carousel_class_name(); $carousel->set_post_id( $post_id ); $settings = self::get_normalized_image_settings( $carousel, $settings ); } // STEP: Populate query vars based on the 'items' settings, if no items are set, no query vars are populated if ( ! empty( $settings['items']['images'] ) ) { $args = [ 'post_status' => 'any', 'post_type' => 'attachment', 'orderby' => 'post__in', 'objectType' => 'post', // @since 1.9.3 'bricks_skip_query_vars' => true, // Skip Query::prepare_query_vars_from_settings() @since 1.9.3 'no_found_rows' => true, // No pagination 'suppress_filters' => true, // WPML @since 2.2 'lang' => '', // Polylang @since 2.2 ]; $images = $settings['items']['images'] ?? $settings['items']; foreach ( $images as $image ) { if ( isset( $image['id'] ) ) { $args['post__in'][] = $image['id']; } } if ( isset( $args['post__in'] ) ) { $args['posts_per_page'] = count( $args['post__in'] ); } } } } return $args; } /** * Check if Query Filters are enabled (in Bricks settings) * * @since 1.9.6 */ public static function enabled_query_filters() { return Database::get_setting( 'enableQueryFilters', false ); } /** * Check if Query Filters integration is enabled in Bricks settings * * @since 1.11.1 */ public static function enabled_query_filters_integration() { return self::enabled_query_filters() && Database::get_setting( 'enableQueryFiltersIntegration', false ); } /** * Implode an array safely to avoid PHP warnings * * @since 1.10 */ public static function safe_implode( $sep, $value ) { // Return: Value is not an array if ( ! is_array( $value ) ) { return (string) $value; } // Flattern the array and convert it to a string $flattern = array_map( function( $item ) use ( $sep ) { if ( is_array( $item ) ) { return self::safe_implode( $sep, $item ); // Recursively flattern the array value } return (string) $item; }, $value ); return implode( $sep, $flattern ); } /** * Builds a hierarchical tree structure from a flat array of Bricks elements. * * The tree structure is used to process each element in a depth-first manner to maintain the hierarchy. * * Each element in the input array should be an associative array with the following structure: * - id (string): Unique identifier for the element. * - name (string): The name/type of the element (e.g., 'section', 'container', 'heading'). * - parent (string|int|null): The ID of the parent element, or 0/null if it is a root element. * - children (array): An array of child element IDs (strings). Defaults to an empty array. * - settings (array): An associative array of settings specific to the element. Defaults to an empty array. * * Example: * [ * [ * "id" => "puikcj", * "name" => "section", * "parent" => 0, * "children" => ["vtjutb"], * "settings" => [] * ], * [ * "id" => "vtjutb", * "name" => "container", * "parent" => "puikcj", * "children" => ["jjnqht", "yvldmi", "cvrpll", "paayqb"], * "settings" => [] * ], * [ * "id" => "jjnqht", * "name" => "heading", * "parent" => "vtjutb", * "children" => [], * "settings" => ["text" => "I am a heading 1"] * ] * // More elements... * ] * * @param array $elements * @return array * * @since 1.10.2 */ public static function build_elements_tree( $elements ) { // Validate input if ( ! is_array( $elements ) || empty( $elements ) ) { self::maybe_log( 'Bricks: Invalid or empty elements array provided to build_elements_tree' ); return []; } $tree = []; $indexed = []; $children_order = []; // First pass: Index elements and store children order foreach ( $elements as $element ) { if ( ! isset( $element['id'] ) || ! is_string( $element['id'] ) ) { self::maybe_log( 'Bricks: Invalid element encountered: missing or invalid ID' ); continue; } // Index the element and initialize its children array $indexed[ $element['id'] ] = $element; $indexed[ $element['id'] ]['children'] = []; // Store the original children order if available if ( isset( $element['children'] ) && is_array( $element['children'] ) ) { $children_order[ $element['id'] ] = array_values( array_filter( $element['children'], 'is_string' ) ); } } // Second pass: Build the hierarchical tree foreach ( $elements as $element ) { if ( ! isset( $element['id'] )) continue; // Skip invalid elements if ( ! empty( $element['parent'] ) && isset( $indexed[ $element['parent'] ] ) ) { // Add as child to parent $indexed[ $element['parent'] ]['children'][] = &$indexed[ $element['id'] ]; } else { // Add to root of tree if no parent or parent not found $tree[] = &$indexed[ $element['id'] ]; } } // Sort children based on the preserved order self::sort_elements_tree_children( $tree, $children_order ); return $tree; } /** * Recursively sorts children of elements based on the original order. * * @param array $elements Array of elements to sort. * @param array $children_order Original order of children for each element. * * @since 1.10.2 */ private static function sort_elements_tree_children( &$elements, $children_order ) { if ( ! is_array( $elements ) ) { self::maybe_log( 'Bricks: Invalid elements provided to sort_children' ); return; } foreach ( $elements as &$element ) { if ( ! isset( $element['id'] ) || ! is_array( $element['children'] ) ) { continue; // Skip invalid elements } if ( ! empty( $element['children'] ) && isset( $children_order[ $element['id'] ] ) ) { $order = array_flip( $children_order[ $element['id'] ] ); usort( $element['children'], function( $a, $b ) use ( $order ) { if ( ! isset( $a['id'] ) || ! isset( $b['id'] ) ) { self::maybe_log( 'Bricks: Invalid child element encountered during sorting' ); return 0; // Don't change order for invalid elements } $pos_a = isset( $order[ $a['id'] ] ) ? $order[ $a['id'] ] : PHP_INT_MAX; $pos_b = isset( $order[ $b['id'] ] ) ? $order[ $b['id'] ] : PHP_INT_MAX; return $pos_a - $pos_b; } ); } // Recursively sort children of this element self::sort_elements_tree_children( $element['children'], $children_order ); } } /** * Get HTML tag name from element settings * * @param array $settings Element settings. * @param string $default_tag Default tag name. * * @since 1.10.2 */ public static function get_html_tag_from_element_settings( $settings, $default_tag ) { $custom_tag = $settings['customTag'] ?? ''; $tag = $settings['tag'] ?? ''; // Use custom tag if set if ( $tag === 'custom' && $custom_tag ) { // Get HTML tag from string (first word, no attributes) $tag = explode( ' ', trim( $custom_tag ) )[0]; } // Allowed HTML tags return self::sanitize_html_tag( $tag, $default_tag ); } /** * Sanitize HTML tag * * @since 1.10.2 */ public static function sanitize_html_tag( $tag, $default_tag ) { // Get allowed HTML tags $allowed_html_tags = self::get_allowed_html_tags(); // Custom tag not allowed: Use default tag if ( ! is_array( $allowed_html_tags ) || ! in_array( $tag, $allowed_html_tags, true ) ) { $tag = $default_tag; } return esc_html( $tag ); } /** * Return all allowed HTML tags * * @since 1.10.3 */ public static function get_allowed_html_tags() { // Allowed HTML tags $allowed_html_tags = array_keys( wp_kses_allowed_html( 'post' ) ); // Filter to extend HTML tag whitelist $extended_allowed_html_tags = apply_filters( 'bricks/allowed_html_tags', $allowed_html_tags ); // Merge default and extended HTML tags $allowed_html_tags = is_array( $extended_allowed_html_tags ) ? array_merge( $allowed_html_tags, $extended_allowed_html_tags ) : $allowed_html_tags; // Remove duplicates $allowed_html_tags = array_unique( $allowed_html_tags ); return array_values( $allowed_html_tags ); } /** * Return all valid pseudo classes * * Pseudo classes source of truth: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes * * @since 1.12 */ public static function get_valid_pseudo_classes() { return [ ':active', ':any-link', ':autofill', ':blank', // Experimental ':checked', ':current', // Experimental ':default', ':defined', ':dir(', // Experimental ':disabled', ':empty', ':enabled', ':first', ':first-child', ':first-of-type', ':fullscreen', ':future', // Experimental ':focus', ':focus-visible', ':focus-within', ':has(', // Experimental ':host', ':host(', ':host-context(', // Experimental ':hover', ':indeterminate', ':in-range', ':invalid', ':is(', ':lang(', ':last-child', ':last-of-type', ':left', ':link', ':local-link', // Experimental ':modal', ':not(', ':nth-child(', ':nth-col(', // Experimental ':nth-last-child(', ':nth-last-col(', // Experimental ':nth-last-of-type(', ':nth-of-type(', ':only-child', ':only-of-type', ':optional', ':out-of-range', ':past', // Experimental ':picture-in-picture', ':placeholder-shown', ':paused', ':playing', ':popover-open', // @since 1.12 ':read-only', ':read-write', ':required', ':right', ':root', ':scope', ':state(', // Experimental ':target', ':target-within', // Experimental ':user-invalid', // Experimental ':valid', ':visited', ':where(', ]; } /** * Return all valid pseudo elements * * Pseudo elements source of truth: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements * * @since 1.12 */ public static function get_valid_pseudo_elements() { return [ '::after', ':after', '::backdrop', ':backdrop', '::before', ':before', '::cue', ':cue', '::cue-region', ':cue-region', '::first-letter', ':first-letter', '::first-line', ':first-line', '::file-selector-button', ':file-selector-button', '::grammar-error', ':grammar-error', // @since 1.12 '::highlight(', ':highlight(', '::marker', ':marker', // @since 1.12: Uncommented '::part(', ':part(', '::placeholder', ':placeholder', '::selection', ':selection', // @since 1.12: Uncommented '::slotted(', ':slotted(', '::spelling-error', ':spelling-error', '::target-text', ':target-text', // @since 1.12 '::view-transition', ':view-transition', // @since 1.12 '::view-transition-image-pair(', ':view-transition-image-pair(', // @since 1.12 '::view-transition-group(', ':view-transition-group(', // @since 1.12 '::view-transition-new', ':view-transition-new', // @since 1.12 '::view-transition-old', ':view-transition-old', ]; } /** * Apply wp_filter_post_kses to all string values in an array * * @since 1.11 */ public static function apply_wp_filter_post_kses_to_array( &$item, $key ) { if ( is_string( $item ) ) { $item = wp_filter_post_kses( $item ); } } /** * Scan site for global class usage * * @since 1.12 * * @return array ['usedClasses' => array, 'unusedClasses' => array] */ public static function scan_global_classes_site_usage() { // Get all global classes $global_classes = get_option( BRICKS_DB_GLOBAL_CLASSES, [] ); if ( ! is_array( Database::$global_data ) ) { Database::$global_data = []; } // Component expansion relies on the global components cache. Database::$global_data['components'] = get_option( BRICKS_DB_COMPONENTS, [] ); // Get array of just the IDs from the global classes objects $class_ids = array_map( function( $class ) { return $class['id']; }, $global_classes ); // Initialize usage tracking $used_classes = []; // Get all posts/templates that use Bricks $post_types = array_merge( array_keys( self::get_supported_post_types() ), [ BRICKS_DB_TEMPLATE_SLUG ] ); $query = new \WP_Query( [ 'post_type' => $post_types, 'posts_per_page' => -1, 'fields' => 'ids', 'post_status' => 'any', 'meta_query' => [ 'relation' => 'OR', [ 'key' => BRICKS_DB_PAGE_HEADER, 'compare' => 'EXISTS', ], [ 'key' => BRICKS_DB_PAGE_CONTENT, 'compare' => 'EXISTS', ], [ 'key' => BRICKS_DB_PAGE_FOOTER, 'compare' => 'EXISTS', ], ], ] ); // Scan each post's Bricks data foreach ( $query->posts as $post_id ) { foreach ( [ 'header', 'content', 'footer' ] as $area ) { self::scan_elements_for_global_classes( Database::get_data( $post_id, $area ), $used_classes ); } } // Also scan global elements $global_elements = get_option( BRICKS_DB_GLOBAL_ELEMENTS, [] ); self::scan_elements_for_global_classes( $global_elements, $used_classes ); // NOTE: Should we also scan BRICKS_DB_PAGE_SETTINGS and BRICKS_DB_GLOBAL_SETTINGS? // These might contain global class references in custom CSS or other settings // Make used classes unique AND only include ones that exist in global classes $used_classes = array_values( array_intersect( array_unique( $used_classes ), // First make unique $class_ids // Then only keep ones that exist in global classes ) ); // Determine unused classes $unused_classes = array_values( array_diff( $class_ids, $used_classes ) ); return [ 'usedClasses' => $used_classes, 'unusedClasses' => $unused_classes ]; } /** * Scan elements and their component instances for global classes * * @since 1.12 * * @param array $elements Array of Bricks elements. * @param array $used_classes Reference to array tracking used class IDs. */ private static function scan_elements_for_global_classes( $elements, &$used_classes ) { $elements = Database::get_component_data( $elements ); if ( ! is_array( $elements ) ) { return; } foreach ( $elements as $element ) { // Check element's global classes if ( ! empty( $element['settings']['_cssGlobalClasses'] ) ) { $element_classes = is_array( $element['settings']['_cssGlobalClasses'] ) ? $element['settings']['_cssGlobalClasses'] : explode( ' ', $element['settings']['_cssGlobalClasses'] ); foreach ( $element_classes as $class_id ) { // Add to array directly instead of using as key if ( $class_id ) { $used_classes[] = $class_id; } } } } } /** * Generate a label for a taxonomy * * @param string $taxonomy * @return string $taxonomy_label * @since 1.12 */ public static function generate_taxonomy_label( $taxonomy ) { $taxonomy_label = ''; $taxonomy_object = get_taxonomy( $taxonomy ); $taxonomy_label = ''; if ( gettype( $taxonomy_object ) === 'object' ) { $taxonomy_label = $taxonomy_object->labels->name; } else { if ( $taxonomy === BRICKS_DB_TEMPLATE_TAX_TAG ) { $taxonomy_label = esc_html__( 'Template tag', 'bricks' ); } if ( $taxonomy === BRICKS_DB_TEMPLATE_TAX_BUNDLE ) { $taxonomy_label = esc_html__( 'Template bundle', 'bricks' ); } } // Avoid empty taxonomy label that will be confusing (@since 1.12) if ( $taxonomy_label === '' ) { $taxonomy_label = ucwords( str_replace( '_', ' ', $taxonomy ) ); } return $taxonomy_label; } /** * Add category metadata to classes for transfer operations * * @param array $classes Array of classes to process * @return array Classes with category metadata * * @since 1.12.2 */ public static function add_category_metadata_to_classes( $classes ) { if ( empty( $classes ) || ! is_array( $classes ) ) { return $classes; } // Get categories from database $categories = get_option( BRICKS_DB_GLOBAL_CLASSES_CATEGORIES, [] ); if ( empty( $categories ) ) { return $classes; } // Add category data for each class that has a category foreach ( $classes as &$class ) { if ( ! empty( $class['category'] ) ) { // Find category data foreach ( $categories as $category ) { if ( $category['id'] === $class['category'] ) { $class['_categoryData'] = [ 'id' => $category['id'], 'name' => $category['name'] ]; break; } } } } return $classes; } /** * Get query object for dynamic tags {query_results_count} and Query Results Summary element * If unable to get query object from history, init query object based on element settings * * Originally located in provider-wp.php * * @since 1.12.2 */ public static function get_query_object_from_history_or_init( $element_id, $post_id ) { $query_object = false; // Element ID provided: Get query object from query history if ( ! empty( $element_id ) ) { $query_object = Query::get_query_by_element_id( $element_id, true ); } else { // No element ID provided, get the current query object $query_object = Query::get_query_object( Query::is_any_looping() ); } // No query object found. Init query (@since 1.9.1.1) if ( ! $query_object ) { // Set $post_id or element_data will be empty (@since 1.10.1; @see #86bzwjx3u) if ( Query::is_any_looping() && isset( Database::$page_data['preview_or_post_id'] ) ) { $post_id = Database::$page_data['preview_or_post_id']; } $element_data = self::get_element_data( $post_id, $element_id ); $element_name = $element_data['element']['name'] ?? ''; // Support query element is a component (root) (@since 1.12.2) if ( ! empty( $element_data['element']['cid'] ) ) { $component_settings = self::get_component_instance( $element_data['element'], 'settings' ); $element_data['element']['settings'] = $component_settings; } if ( ! empty( $element_name ) && isset( $element_data['element']['settings'] ) ) { // Set 'hasLoop' to true for posts element as it has no 'hasLoop' control by default (#86c6g2f54; @since 2.2) if ( in_array( $element_name, [ 'posts', 'carousel', 'related-posts' ], true ) ) { $element_data['element']['settings']['hasLoop'] = true; } // Populate query settings for elements that is not using standard query controls (@since 1.9.3) if ( in_array( $element_name, [ 'carousel', 'related-posts' ] ) ) { $query_settings = self::populate_query_vars_for_element( $element_data['element'], $post_id ); /** * Override query settings. * Carousel 'posts' type should returning empty from this function as it is using standard query controls */ if ( ! empty( $query_settings ) ) { $element_data['element']['settings']['query'] = $query_settings; } /** * If this is a carousel 'posts' type, $query_settings should be empty as the query_settings is populated from standard query controls * However, if the standard query controls is empty (user use default posts query), then we need to set 'query' key so or we are not able to init query in next step */ elseif ( $element_name === 'carousel' && empty( $query_settings ) ) { $carousel_type = $element_data['element']['settings']['type'] ?? 'posts'; if ( $carousel_type === 'posts' && empty( $element_data['element']['settings']['query'] ) ) { $element_data['element']['settings']['query'] = []; } } } // Add query setting key for Posts element when default (no settings), otherwise count always zero (@since 1.9.8) if ( $element_name === 'posts' && empty( $element_data['element']['settings']['query'] ) ) { $element_data['element']['settings']['query'] = []; } // Only init query if query settings is available // @since 2.2: Check hasLoop too as user might turn off loop but leave query settings if ( isset( $element_data['element']['settings']['query'] ) && isset( $element_data['element']['settings']['hasLoop'] ) ) { $query_object = new Query( $element_data['element'] ); if ( $query_object ) { $query_object->destroy(); } } } } return $query_object; } /** * Determine whether to run additional logic to enqueue no results children script * * @since 1.12.2 * * @return bool */ public static function handle_no_results_children_elements() { $run = self::enabled_query_filters(); return apply_filters( 'bricks/handle_no_results_children_elements', $run ); } /** * Get normalized image settings * * @since 2.0 */ public static function get_normalized_image_settings( $instance, $settings ) { $items = $settings['items'] ?? []; $size = $items['size'] ?? BRICKS_DEFAULT_IMAGE_SIZE; // Dynamic data if ( ! empty( $items['useDynamicData'] ) ) { $items['images'] = []; $images = $instance->render_dynamic_data_tag( $items['useDynamicData'], 'image' ); if ( is_array( $images ) ) { foreach ( $images as $image_id ) { $items['images'][] = [ 'id' => $image_id, 'full' => wp_get_attachment_image_url( $image_id, 'full' ), 'url' => wp_get_attachment_image_url( $image_id, $size ) ]; } } } // Old data structure (images were saved as one array directly on $items) if ( ! isset( $items['images'] ) ) { $images = ! empty( $items ) ? $items : []; unset( $items ); $items['images'] = $images; } // Get 'size' from first image if not set $first_image_size = ! empty( $items['images'][0]['size'] ) ? $items['images'][0]['size'] : false; $size = empty( $items['size'] ) && $first_image_size ? $first_image_size : $size; // Get image 'url' for requested $size foreach ( $items['images'] as $key => $image ) { if ( ! empty( $image['id'] ) ) { $items['images'][ $key ]['url'] = wp_get_attachment_image_url( $image['id'], $size ); } } $settings['items'] = $items; $settings['items']['size'] = $size; return $settings; } /** * Check if file is valid SVG * * @param string $file SVG content * * @since 2.0 */ public static function is_valid_svg( $file ) { // Check if $file is a string if ( ! is_string( $file ) ) { return false; } // Check if SVG is valid $file = trim( $file ); // Check if SVG is empty if ( empty( $file ) ) { return false; } // Check if SVG starts with <svg or <xml, and contains </svg> return ( strpos( $file, '<svg' ) === 0 || strpos( $file, '<?xml' ) === 0 ) && strpos( $file, '</svg>' ) !== false; } /** * Send user activation email * * @param number $user_id User ID * @param string $email_type Email type that should be sent (activation, resend-activation) * * @since 2.1 */ public static function send_user_activation_email( $user_id, $email_type ) { // Return: Required data missing if ( ! $user_id || ! isset( $email_type ) || ! in_array( $email_type, [ 'activation', 'resend-activation' ] ) ) { return; } $user = get_user_by( 'id', $user_id ); if ( ! $user ) { return; } // STEP: Prepare global variables $from_email = Database::get_setting( 'userActivationLinkEmailFrom' ); $from_name = Database::get_setting( 'userActivationLinkEmailFromName' ); $subject = Database::get_setting( 'userActivationLinkEmailSubject' ); $content = Database::get_setting( 'userActivationLinkEmailContent' ); $is_html = Database::get_setting( 'userActivationLinkEmailIsHtml', false ); /** * Filter user activation email settings (for translation) * * @since 2.2 */ $from_name = apply_filters( 'bricks/user_activation_email/from_name', $from_name, $user_id ); $subject = apply_filters( 'bricks/user_activation_email/subject', $subject, $user_id ); $content = apply_filters( 'bricks/user_activation_email/content', $content, $user_id ); $additional_params = []; // STEP: Prepare headers $headers = []; // Header: 'From' $from_email = ! empty( $from_email ) ? sanitize_email( $from_email ) : false; if ( $from_email ) { $from_name = ! empty( $from_name ) ? sanitize_text_field( $from_name ) : false; $headers[] = $from_name ? "From: $from_name <$from_email>" : "From: $from_email"; } // Header: 'Content-Type' $headers[] = $is_html ? 'Content-Type: text/html; charset=UTF-8' : 'Content-Type: text/plain; charset=UTF-8'; // STEP: $email_type = activation or resend-activation $activation_key = get_user_meta( $user_id, 'bricks_user_activation_key', true ); // STEP: Prepare activation link $activation_link = ''; $activation_url = ''; $activation_url = add_query_arg( [ 'user_id' => $user_id, 'activation_key' => $activation_key ], // Redirect to custom success page or home page get_permalink( Database::get_setting( 'userActivationLinkSuccessPage', null ) ) ?: home_url() ); $activation_link = $is_html ? '<a href="' . esc_url( $activation_url ) . '" target="_blank">' . esc_html( $activation_url ) . '</a>' : $activation_url; $additional_params['activation_url'] = $activation_url; // https://<site-url>/?params... $additional_params['activation_link'] = $activation_link; // <a href="...">...</a> // Add line breaks if HTML email is enabled and no HTML template exists (@since 1.11.1) if ( $is_html && strpos( $content, '<html' ) === false ) { $content = nl2br( $content ); } $email['subject'] = self::replace_user_activation_email_parameters( $subject, $user, $additional_params, false ); $email['message'] = self::replace_user_activation_email_parameters( $content, $user, $additional_params, $is_html ); if ( empty( $email['subject'] ) ) { $email['subject'] = esc_html__( 'Activate your account', 'bricks' ); } if ( empty( $email['message'] ) ) { $email['message'] = esc_html__( 'Please click the link below to activate your account', 'bricks' ) . ':'; $email['message'] .= $is_html ? "<br>$activation_link" : "\n$activation_url"; } // STEP: Send email $email_sent = wp_mail( $user->user_email, $email['subject'], $email['message'], $headers ); // Log email send error if ( ! $email_sent ) { self::maybe_log( 'Bricks: Failed to send user activation email: ' . print_r( error_get_last(), true ) ); } // STEP: Return return $email_sent; } /** * Set user meta: activation key and status * * @param int $user_id * * @since 2.1 */ public static function set_activation_meta( $user_id ) { $activation_key = null; // "Link" activation key $activation_key = wp_generate_password( 20, false ); // Update user meta with activation key and status update_user_meta( $user_id, 'bricks_user_activation_key', $activation_key ); update_user_meta( $user_id, 'bricks_user_activation_status', 'pending' ); return $activation_key; } /** * Private function, that will replace the available parameters for user activation email * * @param string $content * @param object $user * @param array $additional_params Optional additional parameters * @param bool $is_html Optional flag to indicate if the content is HTML * * @since 2.1 * @return string */ private static function replace_user_activation_email_parameters( $content, $user, $additional_params = [], $is_html = false ) { if ( ! $content || ! $user ) { return $content; } // Site parameters $site_name = get_bloginfo( 'name' ); $site_tagline = get_bloginfo( 'description' ); $site_url = home_url(); // User parameters $username = $user->user_login; $user_display_name = $user->display_name; $user_first_name = $user->first_name; $user_last_name = $user->last_name; $user_email = $user->user_email; // Additional parameters (activation link, activation url,...) $activation_link = isset( $additional_params['activation_link'] ) ? $additional_params['activation_link'] : ''; $activation_url = isset( $additional_params['activation_url'] ) ? $additional_params['activation_url'] : ''; // Replace parameters $search = [ '{{site_name}}', '{{site_tagline}}', '{{site_url}}', '{{username}}', '{{user_display_name}}', '{{user_first_name}}', '{{user_last_name}}', '{{user_email}}', '{{activation_link}}', '{{activation_url}}', ]; $replace = [ $site_name, // Replaces {{site_name}} $site_tagline, // Replaces {{site_tagline}} $site_url, // Replaces {{site_url}} $username, // Replaces {{username}} $user_display_name, // Replaces {{user_display_name}} $user_first_name, // Replaces {{user_first_name}} $user_last_name, // Replaces {{user_last_name}} $user_email, // Replaces {{user_email}} $activation_link, // Replaces {{activation_link}} $activation_url, // Replaces {{activation_url}} ]; // Apply replacement $content = str_replace( $search, $replace, $content ); return $content; } /** * Process settings to update variable references * * @param array $settings Settings to process. * @param string $old_name Old variable name. * @param string $new_name New variable name. * * @return array Updated settings. * @since 2.0 */ public static function process_settings_for_variable_rename( $settings, $old_name, $new_name ) { if ( ! is_array( $settings ) ) { return $settings; } // Create regex pattern to match var(--name) with any whitespace variations $var_pattern = '/var\(\s*--\s*' . preg_quote( $old_name, '/' ) . '\s*\)/'; $var_replacement = 'var(--' . $new_name . ')'; foreach ( $settings as $key => $value ) { // Process nested arrays recursively if ( is_array( $value ) ) { $settings[ $key ] = self::process_settings_for_variable_rename( $value, $old_name, $new_name ); } // Process string values that might contain CSS variable references elseif ( is_string( $value ) ) { $updated_value = preg_replace( $var_pattern, $var_replacement, $value ); if ( $updated_value !== $value ) { $settings[ $key ] = $updated_value; } } } return $settings; } /** * Process elements to update variable references * * @since 2.0 * * @param array $elements Array of elements. * @param string $old_name Old variable name. * @param string $new_name New variable name. * * @return array Updated elements */ public static function process_elements_for_variable_rename( $elements, $old_name, $new_name ) { if ( ! is_array( $elements ) ) { return $elements; } // Process each element's settings foreach ( $elements as $key => $element ) { // Process element settings if ( isset( $element['settings'] ) && is_array( $element['settings'] ) ) { $elements[ $key ]['settings'] = self::process_settings_for_variable_rename( $element['settings'], $old_name, $new_name ); } } return $elements; } /** * Update all references to a renamed global CSS variable across the site * * @since 2.0 * * @param string $old_name Old variable name (without the -- prefix). * @param string $new_name New variable name (without the -- prefix). * * @return array Results with counts of updated items. */ public static function update_global_variable_references( $old_name, $new_name ) { // Initialize results array to track updates $results = [ 'posts_updated' => 0, 'elements_updated' => 0, 'classes_updated' => 0, 'components_updated' => 0, 'settings_updated' => false, 'palette_updated' => false, 'styles_updated' => false, 'template_settings_updated' => false, 'page_settings_updated' => 0, ]; // Skip if old and new names are the same if ( $old_name === $new_name ) { return $results; } // Create regex pattern to match var(--name) with any whitespace variations $var_pattern = '/var\(\s*--\s*' . preg_quote( $old_name, '/' ) . '\s*\)/'; $var_replacement = 'var(--' . $new_name . ')'; // STEP 1: Process posts with Bricks data (header, content, footer) // Get all posts/templates that use Bricks $post_types = array_merge( array_keys( self::get_supported_post_types() ), [ BRICKS_DB_TEMPLATE_SLUG ] ); $query = new \WP_Query( [ 'post_type' => $post_types, 'posts_per_page' => -1, 'fields' => 'ids', 'post_status' => 'any', 'meta_query' => [ 'relation' => 'OR', [ 'key' => BRICKS_DB_PAGE_HEADER, 'compare' => 'EXISTS', ], [ 'key' => BRICKS_DB_PAGE_CONTENT, 'compare' => 'EXISTS', ], [ 'key' => BRICKS_DB_PAGE_FOOTER, 'compare' => 'EXISTS', ], ], ] ); $post_ids = $query->posts; foreach ( $post_ids as $post_id ) { $post_updated = false; // Check header, content, footer foreach ( [ 'header', 'content', 'footer' ] as $area ) { $elements = get_post_meta( $post_id, Database::get_bricks_data_key( $area ), true ); if ( ! empty( $elements ) && is_array( $elements ) ) { $updated_elements = self::process_elements_for_variable_rename( $elements, $old_name, $new_name ); if ( $updated_elements !== $elements ) { update_post_meta( $post_id, Database::get_bricks_data_key( $area ), $updated_elements ); $post_updated = true; $results['elements_updated']++; } } } // Check page settings $page_settings = get_post_meta( $post_id, BRICKS_DB_PAGE_SETTINGS, true ); if ( ! empty( $page_settings ) && is_array( $page_settings ) ) { $updated_settings = self::process_settings_for_variable_rename( $page_settings, $old_name, $new_name ); if ( $updated_settings !== $page_settings ) { update_post_meta( $post_id, BRICKS_DB_PAGE_SETTINGS, $updated_settings ); $post_updated = true; $results['page_settings_updated']++; } } // Check template settings (if this is a template) if ( get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG ) { $template_settings = get_post_meta( $post_id, BRICKS_DB_TEMPLATE_SETTINGS, true ); if ( ! empty( $template_settings ) && is_array( $template_settings ) ) { $updated_settings = self::process_settings_for_variable_rename( $template_settings, $old_name, $new_name ); if ( $updated_settings !== $template_settings ) { update_post_meta( $post_id, BRICKS_DB_TEMPLATE_SETTINGS, $updated_settings ); $post_updated = true; $results['template_settings_updated'] = true; } } } if ( $post_updated ) { $results['posts_updated']++; } } // STEP 2: Process global elements $global_elements = get_option( BRICKS_DB_GLOBAL_ELEMENTS, [] ); if ( is_array( $global_elements ) && ! empty( $global_elements ) ) { $elements_updated = false; foreach ( $global_elements as $key => $element ) { $updated_element = self::process_settings_for_variable_rename( $element, $old_name, $new_name ); if ( $updated_element !== $element ) { $global_elements[ $key ] = $updated_element; $elements_updated = true; $results['elements_updated']++; } } if ( $elements_updated ) { update_option( BRICKS_DB_GLOBAL_ELEMENTS, $global_elements ); } } // STEP 3: Process global classes $global_classes = get_option( BRICKS_DB_GLOBAL_CLASSES, [] ); if ( is_array( $global_classes ) && ! empty( $global_classes ) ) { $classes_updated = false; foreach ( $global_classes as $key => $class ) { if ( isset( $class['settings'] ) && is_array( $class['settings'] ) ) { $updated_settings = self::process_settings_for_variable_rename( $class['settings'], $old_name, $new_name ); if ( $updated_settings !== $class['settings'] ) { $global_classes[ $key ]['settings'] = $updated_settings; $classes_updated = true; $results['classes_updated']++; } } } if ( $classes_updated ) { update_option( BRICKS_DB_GLOBAL_CLASSES, $global_classes ); } } // STEP 4: Process global settings (custom CSS) $global_settings = get_option( BRICKS_DB_GLOBAL_SETTINGS, [] ); if ( is_array( $global_settings ) && ! empty( $global_settings ) ) { $settings_updated = false; // Check custom CSS if ( isset( $global_settings['customCss'] ) ) { $custom_css = $global_settings['customCss']; $updated_css = preg_replace( $var_pattern, $var_replacement, $custom_css ); if ( $updated_css !== $custom_css ) { $global_settings['customCss'] = $updated_css; $settings_updated = true; } } // Check other settings that might contain CSS variables foreach ( $global_settings as $key => $value ) { if ( is_array( $value ) ) { $updated_value = self::process_settings_for_variable_rename( $value, $old_name, $new_name ); if ( $updated_value !== $value ) { $global_settings[ $key ] = $updated_value; $settings_updated = true; } } elseif ( is_string( $value ) ) { $updated_value = preg_replace( $var_pattern, $var_replacement, $value ); if ( $updated_value !== $value ) { $global_settings[ $key ] = $updated_value; $settings_updated = true; } } } if ( $settings_updated ) { update_option( BRICKS_DB_GLOBAL_SETTINGS, $global_settings ); $results['settings_updated'] = true; } } // STEP 5: Process color palette (check for CSS variable references in color values) $color_palette = get_option( BRICKS_DB_COLOR_PALETTE, [] ); if ( is_array( $color_palette ) && ! empty( $color_palette ) ) { $palette_updated = false; foreach ( $color_palette as $palette_key => $palette ) { // Check if this palette has a colors array if ( isset( $palette['colors'] ) && is_array( $palette['colors'] ) ) { foreach ( $palette['colors'] as $color_key => $color ) { // Check for CSS variable references in 'raw' property if ( isset( $color['raw'] ) && is_string( $color['raw'] ) ) { $updated_value = preg_replace( $var_pattern, $var_replacement, $color['raw'] ); if ( $updated_value !== $color['raw'] ) { $color_palette[ $palette_key ]['colors'][ $color_key ]['raw'] = $updated_value; $palette_updated = true; } } } } } if ( $palette_updated ) { update_option( BRICKS_DB_COLOR_PALETTE, $color_palette ); $results['palette_updated'] = true; } } // STEP 6: Process theme styles $theme_styles = get_option( BRICKS_DB_THEME_STYLES, [] ); if ( is_array( $theme_styles ) && ! empty( $theme_styles ) ) { $styles_updated = false; foreach ( $theme_styles as $key => $style ) { if ( isset( $style['settings'] ) && is_array( $style['settings'] ) ) { $updated_settings = self::process_settings_for_variable_rename( $style['settings'], $old_name, $new_name ); if ( $updated_settings !== $style['settings'] ) { $theme_styles[ $key ]['settings'] = $updated_settings; $styles_updated = true; } } } if ( $styles_updated ) { update_option( BRICKS_DB_THEME_STYLES, $theme_styles ); $results['styles_updated'] = true; } } // STEP 7: Process components $components = get_option( BRICKS_DB_COMPONENTS, [] ); if ( is_array( $components ) && ! empty( $components ) ) { $components_updated = false; foreach ( $components as $key => $component ) { if ( isset( $component['elements'] ) && is_array( $component['elements'] ) ) { $updated_elements = self::process_elements_for_variable_rename( $component['elements'], $old_name, $new_name ); if ( $updated_elements !== $component['elements'] ) { $components[ $key ]['elements'] = $updated_elements; $components_updated = true; $results['components_updated']++; } } } if ( $components_updated ) { update_option( BRICKS_DB_COMPONENTS, $components ); } } return $results; } /** * Render a "NEW" badge if BRICKS_VERSION is not larger than the next minor $version * * Example: BRICKS_VERSION = 2.1, $version_added = 2.0 => "New" badge is no longer rendered as its the next minor version. * * @since 2.0 */ public static function render_badge( $version_added ) { // Split version2, fall back to 0 for missing parts $parts = array_map( 'intval', explode( '.', $version_added ) ); $major = $parts[0] ?? 0; $minor = $parts[1] ?? 0; // Build the next-minor version string, patch resets to 0 $next_minor = sprintf( '%d.%d.0', $major, $minor + 1 ); // version_compare understands SemVer, pre-release tags, etc. if ( ! version_compare( $next_minor, BRICKS_VERSION, '<=' ) ) { return '<span class="badge">' . esc_html__( 'New', 'bricks' ) . '</span>'; } } /** * Find orphaned elements across all Bricks posts and templates * * @since 2.0 * * @return array { * @type array $orphaned_by_post_id Array of post IDs with orphaned elements * @type int $total_orphans Total number of orphaned elements found * @type int $total_posts Total number of posts with orphaned elements * } */ public static function find_orphaned_elements_across_site() { $result = [ 'orphaned_by_post_id' => [], 'total_orphans' => 0, 'total_posts' => 0, ]; // Get all Bricks post IDs (templates and content) $template_ids = Templates::get_all_template_ids(); $content_ids = self::get_all_bricks_post_ids(); $all_post_ids = array_merge( $template_ids, $content_ids ); foreach ( $all_post_ids as $post_id ) { $post_orphans = self::find_orphaned_elements_in_post( $post_id ); if ( ! empty( $post_orphans['orphaned_elements'] ) && $post_orphans['total_orphans'] > 0 ) { $result['orphaned_by_post_id'][ $post_id ] = array_merge( $post_orphans, [ 'post_title' => get_the_title( $post_id ), 'permalink' => get_permalink( $post_id ), ] ); $result['total_orphans'] += $post_orphans['total_orphans']; $result['total_posts']++; } } return $result; } /** * Find orphaned elements in a specific post * * @since 2.0 * * @param int $post_id The post ID to check * * @return array { * @type array $orphaned_elements Array of orphaned elements by area * @type int $total_orphans Total number of orphaned elements in this post * } */ public static function find_orphaned_elements_in_post( $post_id ) { $result = [ 'orphaned_elements' => [], 'total_orphans' => 0, ]; $areas_to_check = [ 'content', 'header', 'footer' ]; foreach ( $areas_to_check as $area ) { $elements = Database::get_data( $post_id, $area ); if ( ! is_array( $elements ) || empty( $elements ) ) { continue; } $orphaned_data = self::find_orphaned_elements( $elements ); if ( ! empty( $orphaned_data['orphaned_by_area'] ) ) { $result['orphaned_elements'][ $area ] = $orphaned_data['orphaned_by_area']; $result['total_orphans'] += $orphaned_data['total_orphans']; } } return $result; } /** * Find orphaned elements within an array of elements * * @since 2.0 * * @param array $elements Array of Bricks elements * * @return array { * @type array $orphaned_by_area Array of orphaned elements * @type int $total_orphans Total number of orphaned elements * } */ public static function find_orphaned_elements( $elements ) { if ( ! is_array( $elements ) || empty( $elements ) ) { return [ 'orphaned_by_area' => [], 'total_orphans' => 0, ]; } // Create a map of all element IDs for quick lookup $element_map = []; foreach ( $elements as $element ) { if ( isset( $element['id'] ) ) { $element_map[ $element['id'] ] = $element; } } $orphaned_elements = []; // Check each element for orphaned status foreach ( $elements as $element ) { if ( ! isset( $element['id'] ) || ! isset( $element['parent'] ) ) { continue; } $parent_id = $element['parent']; // Root-level elements have no parent or parent === 0 / '0' if ( ! $parent_id || $parent_id === 0 || $parent_id === '0' ) { continue; } // 1. Check if parent exists if ( ! isset( $element_map[ $parent_id ] ) ) { $orphaned_elements[] = $element['id']; continue; } // 2. Child ID must be in parent's children array $parent = $element_map[ $parent_id ]; $is_child = isset( $parent['children'] ) && is_array( $parent['children'] ) && in_array( $element['id'], $parent['children'], true ); // 3. If not in children, check slotChildren if ( ! $is_child && ! empty( $parent['slotChildren'] ) && is_array( $parent['slotChildren'] ) ) { foreach ( $parent['slotChildren'] as $slot ) { if ( is_array( $slot ) && in_array( $element['id'], $slot, true ) ) { $is_child = true; break; } } } if ( ! $is_child ) { $orphaned_elements[] = $element['id']; } } return [ 'orphaned_by_area' => $orphaned_elements, 'total_orphans' => count( $orphaned_elements ), ]; } /** * Clean up orphaned elements across all posts with orphaned elements * * @since 2.0 * * @param array $orphaned_data Data from find_orphaned_elements_across_site() * * @return array { * @type bool $success True if cleanup was successful * @type int $total_cleaned Total number of elements cleaned up * @type int $posts_cleaned Number of posts that were cleaned up * } */ public static function cleanup_orphaned_elements_across_site( $orphaned_data ) { $total_cleaned = 0; $posts_cleaned = 0; if ( empty( $orphaned_data['orphaned_by_post_id'] ) ) { return [ 'success' => true, 'total_cleaned' => 0, 'posts_cleaned' => 0, ]; } foreach ( $orphaned_data['orphaned_by_post_id'] as $post_id => $post_orphaned_data ) { $cleanup_result = self::cleanup_orphaned_elements_in_post( $post_id, $post_orphaned_data ); if ( $cleanup_result['success'] && $cleanup_result['cleaned_count'] > 0 ) { $total_cleaned += $cleanup_result['cleaned_count']; $posts_cleaned++; } } return [ 'success' => true, 'total_cleaned' => $total_cleaned, 'posts_cleaned' => $posts_cleaned, ]; } /** * Clean up orphaned elements from a specific post * * @since 2.0 * * @param int $post_id The post ID to clean up * @param array $orphaned_data Orphaned elements data from find_orphaned_elements_in_post() * * @return array { * @type bool $success True if cleanup was successful * @type int $cleaned_count Number of elements cleaned up * } */ public static function cleanup_orphaned_elements_in_post( $post_id, $orphaned_data ) { $cleaned_count = 0; if ( empty( $orphaned_data['orphaned_elements'] ) ) { return [ 'success' => true, 'cleaned_count' => 0, ]; } $areas_to_check = [ 'content', 'header', 'footer' ]; foreach ( $areas_to_check as $area ) { if ( ! isset( $orphaned_data['orphaned_elements'][ $area ] ) ) { continue; } $elements = Database::get_data( $post_id, $area ); if ( empty( $elements ) ) { continue; } $orphaned_ids = $orphaned_data['orphaned_elements'][ $area ]; $cleaned_elements = self::cleanup_orphaned_elements( $elements, $orphaned_ids ); // Save cleaned elements back to the post $area_key = Database::get_bricks_data_key( $area ); update_post_meta( $post_id, $area_key, $cleaned_elements ); $cleaned_count += count( $orphaned_ids ); } return [ 'success' => true, 'cleaned_count' => $cleaned_count, ]; } /** * Clean up orphaned elements from an array of elements * * @since 2.0 * * @param array $elements Array of Bricks elements * @param array $orphaned_ids Array of orphaned element IDs to remove * * @return array Cleaned array of elements */ public static function cleanup_orphaned_elements( $elements, $orphaned_ids ) { if ( ! is_array( $elements ) || empty( $elements ) || empty( $orphaned_ids ) ) { return $elements; } // Create element ID map for quick lookup $element_map = []; foreach ( $elements as $element ) { if ( isset( $element['id'] ) ) { $element_map[ $element['id'] ] = $element; } } // Collect all IDs to remove (orphaned elements and their descendants) $ids_to_remove = []; foreach ( $orphaned_ids as $orphaned_id ) { self::collect_descendants( $orphaned_id, $element_map, $ids_to_remove ); } // Filter out orphaned elements and their descendants $cleaned_elements = array_filter( $elements, function( $element ) use ( $ids_to_remove ) { return ! isset( $element['id'] ) || ! in_array( $element['id'], $ids_to_remove ); } ); // Clean up children arrays (remove references to deleted elements) foreach ( $cleaned_elements as $key => $element ) { if ( isset( $element['children'] ) && is_array( $element['children'] ) ) { $cleaned_elements[ $key ]['children'] = array_values( array_filter( $element['children'], function( $child_id ) use ( $ids_to_remove ) { return ! in_array( $child_id, $ids_to_remove ); } ) ); } if ( isset( $element['slotChildren'] ) && is_array( $element['slotChildren'] ) ) { foreach ( $element['slotChildren'] as $slot_key => $slot ) { if ( is_array( $slot ) ) { $cleaned_elements[ $key ]['slotChildren'][ $slot_key ] = array_values( array_filter( $slot, function( $child_id ) use ( $ids_to_remove ) { return ! in_array( $child_id, $ids_to_remove ); } ) ); } } } } // Re-index array to avoid gaps return array_values( $cleaned_elements ); } /** * Recursively collect an element and all its descendants * * @since 2.0 * * @param string $element_id The element ID to collect * @param array $element_map Map of element ID to element data * @param array &$ids_to_remove Reference to array of IDs to remove */ private static function collect_descendants( $element_id, $element_map, &$ids_to_remove ) { if ( in_array( $element_id, $ids_to_remove ) ) { return; } $ids_to_remove[] = $element_id; if ( isset( $element_map[ $element_id ]['children'] ) && is_array( $element_map[ $element_id ]['children'] ) ) { foreach ( $element_map[ $element_id ]['children'] as $child_id ) { self::collect_descendants( $child_id, $element_map, $ids_to_remove ); } } if ( isset( $element_map[ $element_id ]['slotChildren'] ) && is_array( $element_map[ $element_id ]['slotChildren'] ) ) { foreach ( $element_map[ $element_id ]['slotChildren'] as $slot ) { if ( is_array( $slot ) ) { foreach ( $slot as $child_id ) { self::collect_descendants( $child_id, $element_map, $ids_to_remove ); } } } } } /** * If a query is set to use a global query, replace its settings with the global query settings * * @param array $query_settings The query settings to check and potentially replace * @return array The updated query settings * @since 2.1 */ public static function maybe_get_global_query_settings( $query_settings ) { $global_queries = Database::$global_data['globalQueries'] ?? []; $global_query_id = $query_settings['id'] ?? ''; // Find global query by 'id' if ( $global_query_id && ! empty( $global_queries ) ) { $global_query_index = array_search( $global_query_id, array_column( $global_queries, 'id' ) ); if ( $global_query_index !== false ) { $global_query = $global_queries[ $global_query_index ]; // Use global query settings if ( isset( $global_query['settings'] ) && is_array( $global_query['settings'] ) ) { $query_settings = $global_query['settings']; } } } return $query_settings; } /** * Extract name from CSS variable * * Converts "var(--primary)" to "--primary" * Converts "var(--color-1)" to "--color-1" * * @param string $css_var CSS variable string * @return string|false Variable name or false if invalid * * @since 2.2 */ public static function extract_name_from_css_variable( $css_var ) { if ( empty( $css_var ) || ! is_string( $css_var ) ) { return false; } // Remove var() wrapper if present $css_var = trim( $css_var ); $css_var = str_replace( 'var(', '', $css_var ); $css_var = str_replace( ')', '', $css_var ); $css_var = trim( $css_var ); // Ensure it starts with -- if ( strpos( $css_var, '--' ) !== 0 ) { $css_var = '--' . $css_var; } return $css_var; } /** * Get Bricks AJAX endpoint current page * * AJAX pagination: Supported * Query Filters pagination is not supported yet but the code is here for future use * * @since 2.2 */ public static function get_ajax_current_page() { $current_page = false; if ( Api::is_current_endpoint( 'load_query_page' ) ) { $current_page = isset( Api::$request_data['page'] ) ? absint( Api::$request_data['page'] ) : false; } // Query Filters for custom query loop not supported yet if ( self::enabled_query_filters() && Api::is_current_endpoint( 'query_result' ) ) { $current_query_id = Api::$request_data['queryElementId'] ?? false; // Get the value from query filters if ( $current_query_id && isset( Query_Filters::$active_filters[ $current_query_id ] ) ) { $active_filters = Query_Filters::$active_filters[ $current_query_id ]; // Find active filter that query_type is 'pagination' foreach ( $active_filters as $filter ) { if ( isset( $filter['query_type'] ) && $filter['query_type'] === 'pagination' && isset( $filter['query_vars']['paged'] ) ) { $current_page = absint( $filter['query_vars']['paged'] ); break; } } } } return $current_page; } }
修改文件时间
将文件时间修改为当前时间的前一年
删除文件