<?php
namespace wpbuddy\rich_snippets;
if ( ! defined( 'ABSPATH' ) ) {
exit;
} // Exit if accessed directly
/**
* Json object.
*
* @package wpbuddy\rich_snippets
*
* @since 2.0.0
*/
#[\AllowDynamicProperties]
class Rich_Snippet {
/**
* The snippet ID.
*
* @since 2.0.0
*
* @var string
*/
public $id = '';
/**
* The context.
*
* @since 2.0.0
*
* @var string
*/
public $context = 'http://schema.org';
/**
* The type.
*
* @since 2.0.0
*
* @var string
*/
public $type = 'Thing';
/**
* If the object has been prepared for output.
*
* @since 2.0.0
*
* @var bool
*/
private $_is_ready = false;
/**
* Shows if the current snippet is the main/parent snippet.
*
* @since 2.5.4
*
* @var bool
*/
private $_is_main_snippet = false;
/**
* The plugin version the snippet was created with.
*
* @since 2.5.4
*
* @var string
*/
private $_version_created = null;
/**
* If SNIP should iterate over this snippet with a loop.
*
* The output will then create an array of multiple snippets of the same item as this one.
*
* @since 2.8.0
*
* @var string
*/
private $_loop = null;
/**
* The parent snippet ID.
*
* @since 2.14.1
*
* @var null|string
*/
private $_parent_id = null;
/**
* The main (mother) snippet ID (not necessarily the parent).
*
* @since 2.14.1
*
* @var null|string
*/
private $_main_id = null;
/**
* A helper array used for overwriting properties.
*
* @since 2.14.3
* @deprecated 2.14.18
*
* @var array
*/
private $_overwrite_name_helper = [];
/**
* Rich_Snippet constructor.
*
* @param array
*
* @since 2.5.4 Added $args parameter.
*
* @since 2.0.0
*/
public function __construct( $args = [] ) {
foreach ( $args as $arg_key => $arg_value ) {
$this->{$arg_key} = $arg_value;
}
if ( ! array_key_exists( 'id', $args ) ) {
$this->id = uniqid( 'snip-' );
} else {
$this->id = $args['id'];
}
}
/**
* Sets properties.
*
* @param array $props
*
* @since 2.0.0
*
*/
public function set_props( $props = array() ) {
foreach ( $props as $prop ) {
$this->set_prop( $prop['name'], $prop['value'], isset( $prop['id'] ) ? $prop['id'] : null );
}
}
/**
* Sets a single property.
*
* Will add a '-prop-xxx' unique ID to each property that is not a class var.
*
* @param string $name
* @param mixed $value
* @param string|null $id A unique ID for this prop (without the '-prop-' prefix)
*
* @since 2.0.0
*
*/
public function set_prop( $name, $value, $id = null ) {
if ( empty( $id ) ) {
$id = uniqid( '-prop-' );
} else {
if ( false !== stripos( $id, 'prop-' ) ) {
$id = '-' . $id;
} else {
$id = '-prop-' . $id;
}
}
$this->{$name . $id} = $value;
}
/**
* Returns an array of properties.
*
* @return Schema_Property[]
*
* @note This function maybe slow when used on the Frontend as it searches all schemas.
*
* @since 2.0.0
*/
public function get_properties() {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
$props = array();
foreach ( $object_props as $k => $v ) {
$prop_id = $this->normalize_property_name( $k );
$prop_uid = str_replace( $prop_id . '-prop-', '', $k );
$prop_id = 'http://schema.org/' . $prop_id;
$prop = Schemas_Model::get_property_by_id( $prop_id );
if ( $prop instanceof Schema_Property ) {
$prop->value = $v;
$prop->overridable = $v['overridable'] ?? false;
$prop->overridable_multiple = $v['overridable_multiple'] ?? false;
$prop->uid = $prop_uid;
$props[] = $prop;
}
}
return $props;
}
/**
* Get overridable properties.
*
* @param int|null $post_id
* @param string $input_name
* @param string $object_tpe
*
* @return \stdClass[]|Schema_Property[]
*
* @since 2.14.0
* @since 2.14.8 Added $object_type parameter.
*/
public function get_overridable_properties( $post_id = null, $input_name = '', $object_tpe = 'stdClass' ) {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
$props = array();
foreach ( $object_props as $k => $v ) {
if ( isset( $v['overridable'] ) && true === $v['overridable'] ) {
$props[ $k ] = (object) [
'uid' => $this->get_property_uid( $k ),
'label' => $this->normalize_property_name( $k ),
'value' => $v,
];
# do we have this constellation:
# - same property name
# - AND property is overwritable_multiple
# - AND has different sub-schema?
# If YES, we need to set a different label.
$has_same_prop_with_diff_value = call_user_func( function ( $prop_list, $search, $uid ) {
foreach ( $prop_list as $key => $prop ) {
if ( 0 === strpos( $key, $search ) && false === strpos( $key, $search . '-prop-' . $uid ) ) {
return true;
}
}
return false;
}, $object_props, $props[ $k ]->label, $props[ $k ]->uid );
$props[ $k ]->possible_duplicate = $has_same_prop_with_diff_value;
unset( $has_same_prop_with_diff_value, $k, $v );
}
}
unset( $object_props, $class_vars, $object_vars );
if ( count( $props ) <= 0 ) {
return [];
}
/**
* End here means: no data to overwrite.
* OR: no post_id and input_name specified to get the overwritten data from.
*/
if ( is_null( $post_id ) ) {
return $props;
}
if ( empty( $input_name ) ) {
return $props;
}
global $wpdb;
$new_props = $props;
$prop_counter = function ( $props, $name, $uid = null ) {
$i = 0;
foreach ( $props as $prop ) {
if ( $prop->label === $name ) {
if ( is_null( $uid ) ) {
$i ++;
} else {
if ( $prop->uid === $uid ) {
$i ++;
}
}
}
}
return $i;
};
/**
* Find duplicate props
*/
foreach ( $props as $p => $prop ) {
if ( ! $prop->value['overridable_multiple'] ) {
continue;
}
$meta_key = $input_name . $prop->label;
if ( $prop->possible_duplicate ) {
$meta_key .= '-' . $prop->uid . '-';
}
$no = intval( $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM ( SELECT COUNT(*) FROM {$wpdb->postmeta} as pm WHERE post_id = %d AND meta_key REGEXP '{$meta_key}[0-9]+' GROUP BY SUBSTRING_INDEX( SUBSTRING_INDEX( meta_key, %s, -1 ), '_', 1 ) ) as table_a",
$post_id,
$meta_key
) ) );
$no = $no - $prop_counter( $props, $prop->label, $prop->possible_duplicate ? $prop->uid : null );
# back compat
$meta_key_back_compat = $name_back_compat = preg_replace( '#-[0-9a-z]+-#', '', $meta_key );
if ( $meta_key_back_compat !== $meta_key ) {
$no_back_compat = intval( $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM ( SELECT COUNT(*) FROM {$wpdb->postmeta} as pm WHERE post_id = %d AND meta_key REGEXP '{$meta_key_back_compat}[0-9]+' GROUP BY SUBSTRING_INDEX( SUBSTRING_INDEX( meta_key, %s, -1 ), '_', 1 ) ) as table_a",
$post_id,
$meta_key_back_compat
) ) );
$no_back_compat = $no_back_compat - $prop_counter( $props, $prop->label, null );
$no = max( 0, $no, $no_back_compat );
unset( $no_back_compat );
}
if ( $no > 0 ) {
for ( $i = 0; $i < $no; $i ++ ) {
$new_props[] = clone $prop;
}
unset( $i );
}
unset( $p, $prop, $meta_key, $no );
}
unset( $prop_counter );
$name_helper = [];
$name_helper_back_compat = [];
/**
* Fill the values
*/
foreach ( $new_props as $k => $prop ) {
$helper_label = $prop->possible_duplicate ? $prop->label . "-{$prop->uid}-" : $prop->label;
if ( ! isset( $name_helper[ $helper_label ] ) ) {
$name_helper[ $helper_label ] = - 1;
}
$name_helper[ $helper_label ] ++;
# back compat
if ( ! isset( $name_helper_back_compat[ $prop->label ] ) ) {
$name_helper_back_compat[ $prop->label ] = - 1;
}
$name_helper_back_compat[ $prop->label ] ++;
# the input name
# make sure we add the UID if there is another property with the same name
$name = sprintf(
'%s_%s%s%d',
substr( $input_name, 0, strrpos( $input_name, '_' ) ),
$prop->label,
$prop->possible_duplicate ? '-' . $prop->uid . '-' : '', # only add this if we have multiple properties with the same name
$name_helper[ $helper_label ]
);
# back compat
$name_back_compat = preg_replace( '#-[0-9a-z]+-#', '', $name );
# make sure the name fits into the database row
# @todo make this work. Note that the SQL query for finding duplicate rows needs to be rewritten, too!
// if ( strlen( $name ) > 255 ) {
// $elements = preg_split( "#snippet_[0-9]+_#", $name );
// if ( is_array( $elements ) && isset( $elements[1] ) ) {
// $name = str_replace( $elements[1], Helper_Model::instance()->get_short_hash( $elements[1] ), $name );
// }
// }
$new_props[ $k ]->overridable_input_name = $name;
$new_props[ $k ]->overwritten = false;
if ( isset( $prop->value[0] ) && ( 4 === stripos( $prop->value[0], '://schema.org' ) || 5 === stripos( $prop->value[0], '://schema.org' ) ) ) {
# don't fill the value if this is a sub-schema
$value = '';
} else {
$value = get_post_meta( $post_id, $name, true );
}
if ( empty( $value ) ) {
# back compat
if ( $name !== $name_back_compat ) {
$value = get_post_meta( $post_id, $name_back_compat, true );
if ( empty( $value ) ) {
continue;
}
} else {
continue;
}
}
$new_props[ $k ]->value[1] = $value;
$new_props[ $k ]->overwritten = true;
}
unset( $name_helper, $name_helper_back_compat );
# create Schema_Property objects if necessary
if ( 'Schema_Property' === $object_tpe ) {
$props = $new_props;
$new_props = [];
foreach ( $props as $k => $prop ) {
$real_prop = Schemas_Model::get_property_by_id( 'http://schema.org/' . $prop->label );
if ( $real_prop instanceof Schema_Property ) {
$real_prop->value = $prop->value;
$real_prop->overridable = $prop->value['overridable'] ?? false;
$real_prop->overridable_multiple = $prop->value['overridable_multiple'] ?? false;
$real_prop->uid = $prop->uid;
$real_prop->overridable_input_name = $prop->overridable_input_name;
$new_props[ $k ] = $real_prop;
}
}
}
return $new_props;
}
/**
* Removes "-prop-*****" names from property names.
*
* @param string $prop
*
* @return string
* @since 2.0.0
*
*/
private function normalize_property_name( $prop ) {
$prop_id = strstr( $prop, '-prop-', true );
return str_replace( '-prop-', '', $prop_id );
}
/**
* Returns a property value for a given full url (e.g. https://schema.org/image )
*
* @param string $url
*
* @return mixed|null Null if value does not exist.
* @since 2.0.0
*
*/
public function get_property_value_by_path( $url ) {
$url = untrailingslashit( $url );
$val_name = Helper_Model::instance()->remove_schema_url( $url );
if ( isset( $this->{$val_name} ) ) {
return $this->{$val_name};
}
return null;
}
/**
* Returns a property by uid.
*
* @param string $uid
*
* @return bool|Schema_Property
*
* @since 2.14.0
*/
public function get_property_by_uid( $uid ) {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
$object_props_keys = array_keys( $object_props );
foreach ( $object_props_keys as $k ) {
$name = $this->normalize_property_name( $k );
$name = str_replace( $name . '-prop-', '', $k );
if ( $name === $uid ) {
return $this->{$k};
}
}
return false;
}
/**
* Searches the property name when the UID is given.
*
* @param string $uid
*
* @return bool|string
*
* @since 2.14.0
*/
public function get_property_name_by_uid( $uid ) {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
$object_props_keys = array_keys( $object_props );
foreach ( $object_props_keys as $k ) {
$name = $this->normalize_property_name( $k );
$name = str_replace( $name . '-prop-', '', $k );
if ( $name === $uid ) {
return $k;
}
}
return false;
}
/**
* Returns the UID of a property.
*
* @param string $property_name
*
* @return string
*
* @since 2.14.0
*/
public function get_property_uid( $property_name ) {
$name = $this->normalize_property_name( $property_name );
return str_replace( $name . '-prop-', '', $property_name );
}
/**
* Outputs a JSON-String of the object.
*
* @return string
* @since 2.0.0
*
*/
public function __toString(): string {
if ( ! $this->_is_ready ) {
return sprintf( '<!--%s-->',
__( 'Object is not ready for output, yet. Please call \wpbuddy\rich_snippets\Rich_Snippet::prepare_for_output() first.',
'rich-snippets-schema' )
);
}
$prettyprint = (bool) get_option( 'wpb_rs/setting/frontend_json_prettyprint', false );
return json_encode( $this, $prettyprint ? JSON_PRETTY_PRINT : 0 );
}
/**
* Iterates over all items in a loop and prepares the items.
*
* @param array $meta_info
*
* @since 2.8.0
*
*/
private function prepare_loop_items( $meta_info ) {
$vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$props = array_diff_key( $vars, $class_vars );
foreach ( $props as $prop_name_with_id => $prop_value ) {
if ( ! isset( $prop_value[1] ) ) {
continue;
}
/**
* @var Rich_Snippet $child_snippet
*/
$child_snippet = $prop_value[1];
if ( ! $child_snippet instanceof Rich_Snippet ) {
continue;
}
if ( ! $child_snippet->is_loop() ) {
continue;
}
unset( $this->{$prop_name_with_id} );
$prop_name_without_id = $this->normalize_property_name( $prop_name_with_id );
$items = $child_snippet->get_items_for_loop( $meta_info['current_post_id'], $meta_info );
foreach ( $items as $loop_item_id => $loop_item ) {
$snippet = unserialize( serialize( $child_snippet ) );
$snippet->reset_loop();
$item_meta_info = $meta_info;
$item_meta_info['current_post_id'] = $loop_item_id;
$item_meta_info['object'] = $loop_item;
$item_meta_info['in_the_loop'] = true;
$this->set_prop( $prop_name_without_id, $snippet->prepare_for_output( $item_meta_info ) );
}
}
}
/**
* Prepares object for output.
*
* @param array $meta_info
*
* @return \wpbuddy\rich_snippets\Rich_Snippet
*
* @since 2.0.0
*
*/
public function prepare_for_output( array $meta_info = array() ): Rich_Snippet {
$meta_info = wp_parse_args( $meta_info, array(
'current_post_id' => 0,
'snippet_post_id' => 0,
'input' => '',
'queried_object' => null,
) );
if ( $this->_is_ready ) {
return $this;
}
# overwrite values, if any
$this->overwrite_values( $meta_info );
# prepare loop items
$this->prepare_loop_items( $meta_info );
# merge multiple properties together
$this->merge_multiple_props();
# fill all values
$this->fill_values( $meta_info );
# rename some properties
$this->{'@context'} = $this->context;
$this->{'@type'} = $this->type;
# inject custom JSON+LD data
$this->inject_custom_json_ld( $meta_info );
# add "creator" property if needed
if ( $this->_is_main_snippet && (bool) get_option( 'wpb_rs/setting/frontend_json_creator', false ) ) {
$this->{'@context'} = [ $this->{'@context'}, 'snip' => 'https://rich-snippets.io/' ];
$this->{'snip:creator'} = 'SNIP';
}
# delete all internal object vars
foreach ( array_keys( get_class_vars( __CLASS__ ) ) as $k ) {
unset( $this->{$k} );
}
foreach ( array_keys( get_object_vars( $this ) ) as $k ) {
if ( ! isset( $this->{$k} ) ) {
continue;
}
# filter empty props if they are not integers or floats
if ( true === $this->is_property_empty( $k ) ) {
unset( $this->{$k} );
continue;
} else {
# this maybe be an array (which is not fully empty)
if ( is_array( $this->{$k} ) ) {
foreach ( $this->{$k} as $sub_key => $sub_value ) {
if ( $sub_value instanceof Rich_Snippet ) {
if ( $sub_value->is_empty() ) {
unset( $this->{$k}[ $sub_key ] );
}
}
}
}
}
/**
* Workaround: Scalar values need to be transformed to strings.
* This is because the structured data test tools don't like integer values.
*/
if ( is_scalar( $this->{$k} ) ) {
$this->{$k} = (string) $this->{$k};
continue;
}
/**
* Filter empty array values
*/
if ( is_array( $this->{$k} ) ) {
$this->{$k} = array_filter( $this->{$k} );
$this->{$k} = array_values( $this->{$k} );
continue;
}
}
/**
* Rich Snippet Prepare Output Action.
*
* Allows third party plugins to perform any actions after a Snippet has been prepared for output.
*
* @hook wpbuddy/rich_snippets/rich_snippet/prepare
*
* @param {Rich_Snippet} $rich_snippet
*
* @since 2.0.0
*/
do_action_ref_array( 'wpbuddy/rich_snippets/rich_snippet/prepare', array( &$this ) );
$this->_is_ready = true;
return $this;
}
/**
* Prepares the snippet for export.
*
* @since 2.13.3
*/
public function prepare_for_export() {
if ( $this->is_loop() ) {
$this->loop = $this->_loop;
}
foreach ( array_keys( get_object_vars( $this ) ) as $k ) {
if ( isset( $this->{$k} ) && is_array( $this->{$k} ) && isset( $this->{$k}[1] ) && $this->{$k}[1] instanceof Rich_Snippet ) {
$this->{$k}[1]->prepare_for_export();
}
}
}
/**
* Returns the value.
*
* @param mixed $var A key-value pair where the first is the field type and the second is the value itself.
* @param array $meta_info
*
* @return mixed
* @since 2.0.0
*
* @see \wpbuddy\rich_snippets\Admin_Snippets_Controller::search_value_by_id()
*/
private function get_the_value( $var, array $meta_info ) {
$field_type = '';
$overwritten = false;
if ( is_array( $var ) ) {
$overwritten = array_key_exists( 'overwritten', $var ) ? $var['overwritten'] : false;
if ( isset( $var[1] ) && ( $var[1] instanceof Rich_Snippet ) ) {
if ( is_array( $var ) && isset( $var['input_name'] ) ) {
$meta_info['input'] = $var['input_name'] . '_';
}
$var = $var[1]->prepare_for_output( $meta_info );
} else {
$field_type = $var[0];
if ( empty( $field_type ) ) {
return '';
}
$var = isset( $var[1] ) ? $var[1] : '';
}
}
/**
* Rich_Snippet value filter.
*
* Allows plugins to hook into the value that will be outputted later.
*
* @hook wpbuddy/rich_snippets/rich_snippet/value
*
* @param {mixed} $value The value.
* @param {string} $field_type The field type (ie. textfield).
* @param {Rich_Snippet} $object The current Rich_Snippet object.
* @param {array} $meta_info
* @param {bool} $overwritten If the property has been overwritten on a per-post-basis.
*
* @returns {mixed}
*
* @since 2.0.0
*
*/
$var = apply_filters( 'wpbuddy/rich_snippets/rich_snippet/value', $var, $field_type, $this, $meta_info, $overwritten );
# bail early if there is no field type (maybe when the value was overwritten)
if ( empty( $field_type ) ) {
return $var;
}
/**
* Rich_Snippet value type filter.
*
* Allows plugins to hook into the value. The last parameter is the $field_type (ie. textfield).
*
* @hook wpbuddy/rich_snippets/rich_snippet/value/{$field_type}
*
* @param {mixed} $value The value.
* @param {Rich_Snippet} $object The current Rich_Snippet object.
* @param {array} $meta_info
* @param {bool} $overwritten If the property has been overwritten on a per-post-basis.
*
* @returns {mixed}
*
* @since 2.0.0
*/
$var = apply_filters( 'wpbuddy/rich_snippets/rich_snippet/value/' . $field_type, $var, $this, $meta_info, $overwritten );
return $var;
}
/**
* Gets the main type i.e. http://schema.org/Thing
*
* @return string
* @since 2.0.0
*
*/
public function get_type(): string {
return trailingslashit( $this->context ) . $this->type;
}
/**
* Checks if a snippet has properties.
*
* @return bool
* @since 2.0.0
*
*/
public function has_properties(): bool {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
return count( $object_props ) > 1;
}
/**
* Merges multiple props together.
*
* @since 2.0.0
*/
private function merge_multiple_props() {
$vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$props = array_diff_key( $vars, $class_vars );
foreach ( $props as $prop_key => $prop_value ) {
$real_prop_name = $this->normalize_property_name( $prop_key );
if ( ! isset( $this->{$real_prop_name} ) ) {
$this->{$real_prop_name} = $prop_value;
unset( $this->{$prop_key} );
continue;
}
if ( ! $this->{$real_prop_name} instanceof Multiple_Property ) {
# create new Multiple_Property
$mp = new Multiple_Property();
# copy the previous value
$mp[] = $this->{$real_prop_name};
# replace the previous value
$this->{$real_prop_name} = $mp;
}
$this->{$real_prop_name}[] = $prop_value;
unset( $this->{$prop_key} );
}
}
/**
* Fills property values.
*
* @param array $meta_info
*/
private function fill_values( $meta_info ) {
$vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$props = array_diff_key( $vars, $class_vars );
foreach ( $props as $name => $var ) {
if ( ! $this->{$name} instanceof Multiple_Property ) {
$this->{$name} = $this->get_the_value( $var, $meta_info );
} else {
$sub_props = array();
foreach ( $this->{$name} as $sub_prop_key => $sub_prop ) {
$sub_props[ $sub_prop_key ] = $this->get_the_value( $sub_prop, $meta_info );
}
$this->{$name} = $sub_props;
}
}
}
/**
* Checks if the snippet has properties that can be overwritten.
*
* @return bool
*
* @since 2.2.0
*/
public function has_overridable_props() {
$vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$props = array_diff_key( $vars, $class_vars );
foreach ( $props as $name => $var ) {
if ( ! isset( $var['overridable'] ) ) {
continue;
}
if ( $var['overridable'] ) {
return true;
}
}
return false;
}
/**
* Adds necessary IDs to a snippet.
*
* @param Rich_Snippet $snippet
*
* @return Rich_Snippet
*
* @since 2.14.1
*/
public function idfy( $main_id, $parent_id ) {
$this->_parent_id = $parent_id;
$this->_main_id = $main_id;
$vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$props = array_diff_key( $vars, $class_vars );
foreach ( $props as $prop_name_with_id => $prop ) {
if ( ! is_array( $this->{$prop_name_with_id} ) ) {
continue;
}
if ( ! isset( $this->{$prop_name_with_id}[1] ) ) {
continue;
}
if ( ! $this->{$prop_name_with_id}[1] instanceof Rich_Snippet ) {
continue;
}
$this->{$prop_name_with_id}[1]->idfy( $main_id, $this->id );
}
}
/**
* Overwrites values, if any.
*
* @param array $meta_info
*
* @since 2.2.0
*/
public function overwrite_values( $meta_info ) {
if ( empty( $meta_info['current_post_id'] ) ) {
return;
}
/**
* Back compat: old format
*/
$overwrite_data = Helper_Model::instance()->get_properties_to_overwrite( $meta_info['current_post_id'], $this->_main_id, $this->id, $this->_parent_id );
if ( count( $overwrite_data ) > 0 ) {
$did_overwrite = false;
if ( is_array( $overwrite_data ) && count( $overwrite_data ) > 0 ) {
$this->overwrite_properties( $overwrite_data );
$did_overwrite = true;
}
$list_data = Helper_Model::instance()->get_properties_to_list( $meta_info['current_post_id'], $this->_main_id, $this, $this->_parent_id );
if ( is_array( $list_data ) && count( $list_data ) > 0 ) {
foreach ( $list_data as $snippet_id => $ld ) {
$this->create_duplicate( $ld['prop_name'], $ld['properties'], $snippet_id );
}
$did_overwrite = true;
}
if ( $did_overwrite ) {
return;
}
}
/**
* New format
*/
$props = $this->get_overridable_properties( $meta_info['current_post_id'], $meta_info['input'] );
if ( count( $props ) <= 0 ) {
return;
}
$props_to_delete = [];
foreach ( $props as $prop ) {
$props_to_delete[] = $prop->uid;
$prop_value = unserialize( serialize( $prop->value ) );
$prop_value['input_name'] = isset( $prop->overridable_input_name ) ? $prop->overridable_input_name : '';
if ( isset( $prop->overwritten ) && $prop->overwritten ) {
# avoid overwriting
$prop_value['overwritten'] = true;
}
$this->set_prop( $prop->label, $prop_value );
}
$props_to_delete = array_filter( $props_to_delete );
foreach ( $props_to_delete as $prop_to_delete ) {
$prop_name = $this->get_property_name_by_uid( $prop_to_delete );
if ( isset( $this->{$prop_name} ) ) {
unset( $this->{$prop_name} );
}
}
}
/**
* Integrates custom JSON+LD values for the main snippet.
*
* @param array $meta_info
*
* @since 2.4.0
*/
private function inject_custom_json_ld( $meta_info ) {
if ( ! isset( $meta_info['snippet_post_id'] ) ) {
return;
}
if ( empty( $meta_info['snippet_post_id'] ) ) {
return;
}
if ( ! $this->is_main_snippet() ) {
return;
}
$json_ld_data = (array) get_post_meta( $meta_info['snippet_post_id'], '_wpb_rs_jsonld', true );
/**
* Custom JSON+LD filter.
*
* Allows to hook into custom JSON+LD code.
*
* @hook wpbuddy/rich_snippets/rich_snippet/json+ld
*
* @param {array} $json_ld_data
* @param {array} $meta_info
*
* @returns {array}
*
* @since 2.4.0
*/
$json_ld_data = apply_filters( 'wpbuddy/rich_snippets/rich_snippet/json+ld', $json_ld_data, $meta_info );
$json_ld_data = array_filter( $json_ld_data );
foreach ( $json_ld_data as $key => $value ) {
/**
* Custom JSON+LD value filter.
*
* Allows to change a custom JSON+LD value.
*
* @hook wpbuddy/rich_snippets/rich_snippet/json+ld/value
*
* @param {mixed} $value
* @param {mixed} $meta_info
*
* @since 2.4.0
*
* @returns {mixed}
*/
$value = apply_filters( 'wpbuddy/rich_snippets/rich_snippet/json+ld/value', $value, $meta_info );
/**
* Dynamic custom JSON+LD value filter.
*
* Allows to change a custom JSON+LD value.
*
* @hook wpbuddy/rich_snippets/rich_snippet/json+ld/value/{key}
*
* @param {mixed} $value
* @param {mixed} $meta_info
*
* @since 2.4.0
*
* @returns {mixed}
*/
$value = apply_filters( 'wpbuddy/rich_snippets/rich_snippet/json+ld/value/' . $key, $value, $meta_info );
if ( empty( $value ) ) {
continue;
}
$this->{$key} = $value;
}
}
/**
* Get the plugin version the snippet was created with.
*
* @return string x.x.x format.
* @since 2.5.4
*
*/
public function get_version_created() {
return $this->_version_created ?? null;
}
/**
* Sets the _is_main_snippet property. But only if the snippet was created with plugin version number 2.5.3 or
* lower (meaning, the version string is empty).
*
* @param $val
*
* @deprecated 2.4.5
*
* @since 2.5.4
*
*/
public function set_is_main_snippet( $val ) {
$plugin_version = $this->get_version_created();
if ( empty( $plugin_version ) ) {
$this->_is_main_snippet = $val;
}
}
/**
* If the current snippet is the main/parent snippet.
*
* @return bool
* @since 2.5.4
*
*/
public function is_main_snippet() {
return $this->_is_main_snippet ?? false;
}
/**
* If a loop is configured for this snippet.
*
* @return bool
* @since 2.8.0
*
*/
public function is_loop() {
return ! empty( $this->_loop );
}
/**
* The type of the loop.
*
* @return string
* @since 2.8.0
*
*/
public function get_loop_type() {
return $this->_loop;
}
/**
* Returns the loop items.
*
* @param int $post_id
*
* @return mixed[] Array of items (could be objects)
*
* @since 2.8.0
* @since 2.25.2 Added $meta_info
*/
public function get_items_for_loop( $post_id, $meta_info ) {
$items = [];
if ( 'main_query' === $this->_loop ) {
global $wp_the_query;
if ( isset( $wp_the_query ) && $wp_the_query instanceof \WP_Query ) {
$items = $wp_the_query->get_posts();
}
$items = array_combine( wp_list_pluck( $items, 'ID' ), $items );
} elseif ( 'page_parents' === $this->_loop ) {
$items = get_post_ancestors( $post_id ); #@todo test this again
$items = array_reverse( $items );
$items = array_combine( $items, $items );
} elseif ( 0 === stripos( $this->_loop, 'menu_' ) ) {
$menu_name = str_replace( 'menu_', '', $this->_loop );
$items = wp_get_nav_menu_items( $menu_name );
$menu_id = call_user_func( function ( $items, $id ) {
foreach ( $items as $item ) {
if ( isset( $item->object_id ) && $item->object_id == $id ) {
return $item->ID;
}
}
return $id;
}, $items, $post_id );
// $items = Helper_Model::instance()->filter_item_hierarchy(
// $items,
// $menu_id,
// 'menu_item_parent',
// 'ID'
// );
$items = array_reverse( $items );
$items = array_combine( wp_list_pluck( $items, 'object_id' ), $items );
} elseif ( 0 === stripos( $this->_loop, 'taxonomy_' ) ) {
$taxonomy = str_replace( 'taxonomy_', '', $this->_loop );
if ( 0 === $post_id && is_archive() ) {
# On archive pages, fetch the queried object id
$term_id = get_queried_object_id();
} else {
if ( 'category' === $taxonomy ) {
$term_id = Helper_Model::instance()->get_primary_category( $post_id );
} else {
$term_id = Helper_Model::instance()->get_primary_term( $taxonomy, $post_id );
}
}
if ( $term_id > 0 ) {
$items = get_ancestors( $term_id, $taxonomy, 'taxonomy' );
$items = array_reverse( $items );
$items[] = $term_id;
array_walk( $items, function ( &$term_id, $key, $taxonomy ) {
$term = get_term( $term_id, $taxonomy );
if ( ! $term instanceof \WP_Term ) {
$term_id = null;
return null;
}
$term_id = $term;
}, $taxonomy );
$items = array_filter( $items );
$items = array_combine( wp_list_pluck( $items, 'term_id' ), $items );
}
}
/**
* Loop Items filer.
*
* Add/Change loop items.
*
* @hook wpbuddy/rich_snippets/rich_snippet/loop/items
*
* @param {array} $items
* @param {Rich_Snippet} $this
* @param {int} $post_id
*
* @returns {array} They key should be the item ID. The value the object of the item.
* @since 2.8.0
* @since 2.25.2 Added $meta_info
*/
return apply_filters( 'wpbuddy/rich_snippets/rich_snippet/loop/items', $items, $this, $post_id, $meta_info );
}
/**
* Resets the loop to NULL.
*
* @return void
* @since 2.8.0
*/
public function reset_loop() {
$this->_loop = null;
}
/**
* Checks if all properties are empty.
*
* @return bool
*
* @since 2.12.1
*/
public function is_empty() {
if ( ! $this->_is_ready ) {
return false;
}
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_diff_key( $object_vars, $class_vars );
unset( $object_props['@context'] );
unset( $object_props['@type'] );
foreach ( array_keys( $object_props ) as $k ) {
if ( ! $this->is_property_empty( $k ) ) {
return false;
}
}
return true;
}
/**
* Checks if a property is empty.
*
* @param string $property_name
*
* @return bool
* @since 2.12.1
*
*/
public function is_property_empty( string $property_name ) {
if ( ! isset( $this->{$property_name} ) ) {
return null;
}
if ( '' === $this->{$property_name} ) {
return true;
}
if ( ! ( is_int( $this->{$property_name} ) || is_float( $this->{$property_name} ) ) && empty( $this->{$property_name} ) && ! is_array( $this->{$property_name} ) ) {
# do not strip number entered by a user (recognizable by a string)
if ( ! ctype_digit( $this->{$property_name} ) ) {
return true;
}
}
if ( $this->{$property_name} instanceof Rich_Snippet && $this->{$property_name}->is_empty() ) {
return true;
}
if ( is_array( $this->{$property_name} ) ) {
$empty_no = 0;
foreach ( $this->{$property_name} as $sub_prop ) {
if ( ! $sub_prop instanceof Rich_Snippet ) {
continue;
}
if ( $sub_prop->is_empty() ) {
$empty_no ++;
}
}
return $empty_no === count( $this->{$property_name} );
}
return false;
}
/**
* Overwrites properties.
*
* @param array $properties
*
* @since 2.14.0
*
* @dperecated 2.14.3
*/
public function overwrite_properties( $properties ) {
foreach ( $properties as $property ) {
$name = $this->get_property_name_by_uid( $property['prop_id'] );
if ( false === $name ) {
continue;
}
if ( ! ( isset( $this->{$name}['overridable'] ) && $this->{$name}['overridable'] ) ) {
continue;
}
if ( isset( $this->{$name}['overridable_multiple'] ) && $this->{$name}['overridable_multiple'] && isset( $this->{$name}['overwritten'] ) && $this->{$name}['overwritten'] ) {
# if it's overwritten, we need to create a new prop.
$copy = $this->{$name};
$copy[0] = 'overwrite'; # this makes sure that overwritten properties do not get overwritten again later in the "get_value" function
$copy[1] = $property['value'];
$copy['list_item'] = true;
$copy['original_prop_id'] = $property['prop_id'];
$this->set_prop( $this->normalize_property_name( $name ), $copy );
unset( $copy );
} else {
$this->{$name}[0] = 'overwrite'; # this makes sure that overwritten properties do not get overwritten again later in the "get_value" function
$this->{$name}[1] = $property['value'];
$this->{$name}['overwritten'] = true;
}
}
}
/**
* Returns all snippet IDs from properties that are Rich_Snippets themselves.
*
* @return array
* @since 2.14.0
*/
public function get_sub_snippet_ids() {
$object_vars = get_object_vars( $this );
$class_vars = get_class_vars( __CLASS__ );
$object_props = array_keys( array_diff_key( $object_vars, $class_vars ) );
$d = [];
foreach ( $object_props as $name ) {
if ( $this->{$name}[1] instanceof Rich_Snippet ) {
$d[ $name ] = $this->{$name}[1]->id;
}
}
return $d;
}
public function get_sub_snippet_ids_deep() {
$sub_snippets_ids = $this->get_sub_snippet_ids();
$all = [];
foreach ( $sub_snippets_ids as $prop_name_and_id => $sub_snippet_id ) {
$more = $this->{$prop_name_and_id}[1]->get_sub_snippet_ids_deep();
$all = array_merge( $all, $more );
}
return array_merge( $sub_snippets_ids, $all );
}
/**
* Duplicates properties.
*
* @param string $prop_name
* @param array $properties
* @param string $snippet_id
*
* @since 2.14.0
*
* @deprecated 2.14.3
*/
public function create_duplicate( $prop_name, $properties, $snippet_id ) {
if ( ! isset( $this->{$prop_name} ) ) {
return;
}
# make sure this is not copied by reference
$duplicate = unserialize( serialize( $this->{$prop_name} ) );
if ( ! $duplicate[1] instanceof Rich_Snippet ) {
return;
}
$duplicate[1]->id = $snippet_id;
$duplicate[1]->overwrite_properties( $properties );
$name = $this->normalize_property_name( $prop_name );
$this->set_prop( $name, $duplicate );
}
/**
* Returns the main snippet (if in a loop).
*
* @return string|null
* @since 2.14.27
*/
public function get_main_snippet_id() {
return $this->_main_id;
}
}