| Server IP : 80.74.154.100 / Your IP : 216.73.217.0 Web Server : Apache System : Linux marissa.metanet.ch 4.18.0-553.141.2.lve.el7h.x86_64 #1 SMP Wed Jul 8 17:20:31 UTC 2026 x86_64 User : onlineadmin ( 10487) PHP Version : 8.5.7 Disable Function : opcache_get_status MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /home/httpd/vhosts/comeonline.ch/httpdocs/wp-content/plugins/wpsso/lib/ |
Upload File : |
<?php
/*
* License: GPLv3
* License URI: https://www.gnu.org/licenses/gpl.txt
* Copyright 2012-2026 Jean-Sebastien Morisset (https://wpsso.com/)
*/
if ( ! defined( 'ABSPATH' ) ) {
die( 'These aren\'t the droids you\'re looking for.' );
}
if ( ! defined( 'WPSSO_PLUGINDIR' ) ) {
die( 'Do. Or do not. There is no try.' );
}
if ( ! class_exists( 'WpssoSchemaGraph' ) ) {
require_once WPSSO_PLUGINDIR . 'lib/schema-graph.php';
}
if ( ! class_exists( 'WpssoSchemaSingle' ) ) {
require_once WPSSO_PLUGINDIR . 'lib/schema-single.php';
}
if ( ! class_exists( 'WpssoSchema' ) ) {
class WpssoSchema {
private $p; // Wpsso class object.
private $types_cache = array(); // Schema types array cache.
private $init_json_prio = -1000; // 'wpsso_init_json_filters' action priority.
public function __construct( &$plugin ) {
$this->p =& $plugin;
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
/*
* To optimize performance and memory usage, the 'wpsso_init_json_filters' action is run at the start of
* WpssoSchema->get_json_data(), when the Schema filters are required. The action then unhooks itself so it
* can only be run once.
*/
$this->p->util->add_plugin_actions( $this, array(
'init_json_filters' => 0,
), $this->init_json_prio );
$this->p->util->add_plugin_filters( $this, array(
'plugin_image_sizes' => 1,
'sanitize_md_defaults' => 2,
'sanitize_md_options' => 2,
), $prio = 5 );
add_action( 'wp_ajax_wpsso_schema_type_og_type', array( $this, 'ajax_schema_type_og_type' ) );
}
public function action_init_json_filters() {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'init json filters' ); // Begin timer.
}
$classnames = $this->p->get_lib_classnames( 'json' ); // Always returns an array.
foreach ( $classnames as $id => $classname ) {
/*
* Since WPSSO Core v15.0.0.
*
* Example $filter_name = 'wpsso_init_json_filter_prop_haspart'.
*/
$filter_name = SucomUtil::sanitize_hookname( 'wpsso_init_json_filter_' . $id );
if ( apply_filters( $filter_name, true ) ) new $classname( $this->p );
}
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'init json filters' ); // End timer.
$this->p->debug->log( 'removing ' . __FUNCTION__ . ' method action' );
}
/*
* Just in case, unhook the method to make sure Schema filters are only loaded once.
*/
remove_action( 'wpsso_init_json_filters', array( $this, __FUNCTION__ ), $this->init_json_prio );
}
public function filter_plugin_image_sizes( array $sizes ) {
$sizes[ 'schema_1x1' ] = array( // Option prefix.
'name' => 'schema-1x1',
'label_transl' => _x( 'Schema 1:1 (Google Rich Results)', 'option label', 'wpsso' ),
);
if ( apply_filters( 'wpsso_add_image_sizes_schema_4x3', true ) ) {
$sizes[ 'schema_4x3' ] = array( // Option prefix.
'name' => 'schema-4x3',
'label_transl' => _x( 'Schema 4:3 (Google Rich Results)', 'option label', 'wpsso' ),
);
}
if ( apply_filters( 'wpsso_add_image_sizes_schema_16x9', true ) ) {
$sizes[ 'schema_16x9' ] = array( // Option prefix.
'name' => 'schema-16x9',
'label_transl' => _x( 'Schema 16:9 (Google Rich Results)', 'option label', 'wpsso' ),
);
}
if ( apply_filters( 'wpsso_add_image_sizes_schema_thumb', true ) ) {
$sizes[ 'thumb' ] = array( // Option prefix.
'name' => 'thumbnail',
'label_transl' => _x( 'Schema Thumbnail', 'option label', 'wpsso' ),
);
}
return $sizes;
}
public function filter_sanitize_md_defaults( $md_defs, $mod ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
return $this->filter_sanitize_md_options( $md_defs, $mod );
}
public function filter_sanitize_md_options( $md_opts, $mod ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
if ( function_exists( 'is_sitemap' ) && is_sitemap() ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'skipping sanitizing md defaults for sitemap' );
}
return $md_opts;
}
if ( ! empty( $mod[ 'is_post' ] ) ) {
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_adult_type', $enum_key = 'adult_type',
$val_prefix = '', $val_suffix = 'Consideration' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_age_group', $enum_key = 'age_group' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_avail', $enum_key = 'item_availability' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_condition', $enum_key = 'item_condition',
$val_prefix = '', $val_suffix = 'Condition' );
foreach ( SucomUtil::preg_grep_keys( '/^product_energy_efficiency(_min|_max)?$/', $md_opts ) as $prop_name => $prop_val ) {
self::check_prop_value_enumeration( $md_opts, $prop_name, $enum_key = 'energy_efficiency',
$val_prefix = 'EUEnergyEfficiencyCategory' );
}
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_price_type', $enum_key = 'price_type' );
foreach ( SucomUtil::preg_grep_keys( '/^product_size_group_[0-9]+$/', $md_opts ) as $prop_name => $prop_val ) {
self::check_prop_value_enumeration( $md_opts, $prop_name, $enum_key = 'size_group',
$val_prefix = 'WearableSizeGroup' );
}
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_size_system', $enum_key = 'size_system',
$val_prefix = 'WearableSizeSystem' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'product_target_gender', $enum_key = 'target_gender' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'schema_event_attendance', $enum_key = 'event_attendance' );
self::check_prop_value_enumeration( $md_opts, $prop_name = 'schema_event_status', $enum_key = 'event_status' );
/*
* Check offer availability values and skip any ':disabled' status keys.
*/
foreach ( SucomUtil::preg_grep_keys( '/^schema_(.*)_offer_avail[^:]*$/', $md_opts ) as $prop_name => $prop_val ) {
self::check_prop_value_enumeration( $md_opts, $prop_name, $enum_key = 'item_availability' );
}
}
return $md_opts;
}
/*
* Returns the language and country code, like "en_US".
*
* If the $trim_lang argument value is true, then return the 2 character primary language instead, like "en".
*
* Note that some Chinese languages will return a 5 character string instead, like 'zh-cn' or 'zh-tw'.
*
* $mixed = 'default' | 'current' | post ID | $mod array
*
* See WpssoUtil->get_sitemaps_news().
*/
public function get_schema_lang( $mixed = 'current', $trim_lang = false ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
$schema_lang = SucomUtilWP::get_locale( $mixed );
/*
* If there is a multilingual plugin available, skip any custom language value.
*/
if ( empty( $this->p->avail[ 'lang' ][ 'any' ] ) ) {
if ( ! empty( $mixed[ 'obj' ] ) && $mixed[ 'id' ] ) {
$schema_type_id = $this->get_mod_schema_type_id( $mixed );
if ( $this->is_schema_type_child( $schema_type_id, 'creative.work' ) ) {
$custom_schema_lang = $mixed[ 'obj' ]->get_options( $mixed[ 'id' ], 'schema_lang' );
/*
* Check that the id value is not true, false, null, empty string, or 'none'.
*/
if ( SucomUtil::is_valid_option_value( $custom_schema_lang ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'custom schema_lang = ' . $custom_schema_lang );
}
$schema_lang = $custom_schema_lang;
}
}
}
}
if ( $trim_lang ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting primary language for ' . $schema_lang );
}
/*
* Exceptions.
*
* See https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap.
*/
switch ( $schema_lang ) {
case 'zh_CN': // Simplified Chinese (China).
$schema_lang = 'zh-cn';
break;
case 'zh_HK': // Traditional Chinese (Hong Kong).
case 'zh_TW': // Traditional Chinese (Taiwan).
$schema_lang = 'zh-tw';
break;
default:
if ( function_exists( 'locale_get_primary_language' ) ) { // Requires the PHP Intl package.
$schema_lang = locale_get_primary_language( $schema_lang );
} else $schema_lang = substr( $schema_lang, 0, 2 );
break;
}
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning schema_lang = ' . $schema_lang );
}
$schema_lang = apply_filters( 'wpsso_schema_lang', $schema_lang, $mixed, $trim_lang );
return $schema_lang;
}
/*
* Called by WpssoHead->get_head_array().
*
* Pass $mt_og by reference to assign values to the schema:* internal meta tags.
*/
public function get_array( array $mod, array &$mt_og = array() ) { // Pass by reference is OK.
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'build schema array' ); // Begin timer.
}
/*
* See WpssoSchema->add_schema_mt_og().
*/
$page_language = $mt_og[ 'schema:language' ] = $this->get_schema_lang( $mod );
$page_type_id = $mt_og[ 'schema:type:id' ] = $this->get_mod_schema_type_id( $mod ); // Example: article.tech.
$page_type_url = $mt_og[ 'schema:type:url' ] = $this->get_schema_type_url( $page_type_id ); // Example: https://schema.org/TechArticle.
list(
$mt_og[ 'schema:type:context' ],
$mt_og[ 'schema:type:name' ],
$mt_og[ 'schema:type:path' ],
) = $this->get_schema_type_url_parts( $page_type_url );
$page_type_ids = array();
$page_type_added = array(); // Prevent duplicate schema types.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'head schema type id is ' . $page_type_id . ' (' . $page_type_url . ')' );
}
/*
* Include Schema Organization or Person, and Schema WebSite markup on the home page.
*/
if ( $mod[ 'is_home' ] ) { // Home page (static or blog archive).
switch ( $this->p->options[ 'site_pub_schema_type' ] ) {
case 'organization':
$site_org_type_id = $this->p->options[ 'site_org_schema_type' ]; // Organization or a sub-type of organization.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'organization schema type id is ' . $site_org_type_id );
}
if ( $page_type_id !== $site_org_type_id && ! $this->is_schema_type_child( $page_type_id, $site_org_type_id ) ) {
$page_type_ids[ $site_org_type_id ] = true;
}
break;
case 'person':
$page_type_ids[ 'person' ] = true;
break;
}
$page_type_ids[ 'website' ] = true;
}
/*
* Could be an organization, website, or person, so include last to reenable (if disabled by default).
*/
if ( ! empty( $page_type_url ) ) {
$page_type_ids[ $page_type_id ] = true;
}
/*
* Array (
* [product] => true
* [website] => true
* [organization] => true
* [person] => false
* )
*
* Hooked by WpssoBcFilters->filter_json_array_schema_page_type_ids() to add its 'breadcrumb.list' type id.
*/
$page_type_ids = apply_filters( 'wpsso_json_array_schema_page_type_ids', $page_type_ids, $mod );
if ( $this->p->debug->enabled ) {
$this->p->debug->log_arr( 'page_type_ids', $page_type_ids );
}
/*
* Start a new @graph array.
*/
WpssoSchemaGraph::reset_data();
foreach ( $page_type_ids as $type_id => $is_enabled ) {
if ( ! $is_enabled ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'skipping schema type id "' . $type_id . '" (disabled)' );
}
continue;
} elseif ( ! empty( $page_type_added[ $type_id ] ) ) { // Prevent duplicate schema types.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'skipping schema type id "' . $type_id . '" (previously added)' );
}
continue;
} else $page_type_added[ $type_id ] = true; // Prevent adding duplicate schema types.
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'schema type id ' . $type_id ); // Begin timer.
}
if ( $type_id === $page_type_id ) { // This is the main entity.
$is_main = true;
} else $is_main = false; // Default for all other types.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'schema main entity is ' . ( $is_main ? 'true' : 'false' ) . ' for ' . $type_id );
}
/*
* WpssoSchema->get_json_data() returns a two dimensional array of json data unless $single is true.
*/
$json_data = $this->get_json_data( $mod, $mt_og, $type_id, $is_main, $single = false );
/*
* Add the json data to the @graph array.
*/
foreach ( $json_data as $num => $single_graph ) {
if ( empty( $single_graph ) || ! is_array( $single_graph ) ) { // Just in case.
continue;
}
if ( empty( $single_graph[ '@type' ] ) ) {
$type_url = $this->get_schema_type_url( $type_id ); // Returns false if the Schema type id not found.
$single_graph = self::get_schema_type_context( $type_url, $single_graph );
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'added @type property is ' . $single_graph[ '@type' ] );
}
} elseif ( $this->p->debug->enabled ) {
$this->p->debug->log( 'existing @type property is ' . print_r( $single_graph[ '@type' ], true ) );
}
$single_graph = apply_filters( 'wpsso_json_data_graph_element', $single_graph, $mod, $mt_og, $page_type_id, $is_main );
/*
* Add meta tags (like 'schema:review:rating') based on the filtered Schema markup.
*/
$this->add_schema_mt_og( $single_graph, $mod, $mt_og, $page_type_id, $is_main );
WpssoSchemaGraph::add_data( $single_graph );
}
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'schema type id ' . $type_id ); // End timer.
}
unset( $json_data );
}
/*
* Get the @graph json array and start a new @graph array.
*/
$graph_type_url = WpssoSchemaGraph::get_type_url();
$graph_json = WpssoSchemaGraph::get_json_reset_data(); // Get and reset the static data.
$filter_name = SucomUtil::sanitize_hookname( 'wpsso_json_prop_' . $graph_type_url );
$graph_json = apply_filters( $filter_name, $graph_json, $mod, $mt_og );
$schema_scripts = array();
if ( ! empty( $graph_json[ '@graph' ] ) ) { // Just in case.
if ( SucomUtil::get_const( 'WPSSO_SCHEMA_GRAPH_OPTIMIZE_DISABLE' ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'WPSSO_SCHEMA_GRAPH_OPTIMIZE_DISABLE is true' );
}
} else WpssoSchemaGraph::optimize_json( $graph_json );
$schema_scripts[][] = '<script type="application/ld+json" id="wpsso-schema-graph">' .
$this->p->util->json_format( $graph_json ) . '</script>' . "\n";
}
unset( $graph_json );
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'build schema array' ); // End timer.
$this->p->debug->log( 'applying filters "wpsso_schema_scripts"' );
}
$schema_scripts = apply_filters( 'wpsso_schema_scripts', $schema_scripts, $mod, $mt_og );
return $schema_scripts;
}
/*
* Get the JSON-LD data array.
*
* Returns a two dimensional array of json data unless $single is true.
*/
public function get_json_data( array $mod, array $mt_og, $page_type_id = false, $is_main = false, $single = false ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
/*
* To optimize performance and memory usage, the 'wpsso_init_json_filters' action is run at the start of
* WpssoSchema->get_json_data(), when the Schema filters are required.
*/
static $do_once = null;
if ( null === $do_once ) {
$do_once = true;
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'doing action "wpsso_init_json_filters"' );
}
do_action( 'wpsso_init_json_filters' );
}
if ( empty( $page_type_id ) ) {
$page_type_id = $this->get_mod_schema_type_id( $mod );
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'page type id is ' . $page_type_id );
}
}
/*
* Returns an array of type ids with gparents, parents, child (in that order).
*/
$child_family_urls = array();
foreach ( $this->get_schema_type_child_family( $page_type_id ) as $type_id ) {
$child_family_urls[] = $this->get_schema_type_url( $type_id ); // Returns false if the Schema type id not found.
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log_arr( 'child_family_urls', $child_family_urls );
}
$json_data = null;
foreach ( $child_family_urls as $num => $type_url ) {
$type_hookname = SucomUtil::sanitize_hookname( $type_url );
$data_filter_name = 'wpsso_json_data_' . $type_hookname;
$valid_filter_name = 'wpsso_json_data_validate_' . $type_hookname;
$method_filter_name = 'filter_json_data_' . $type_hookname;
/*
* Add website, organization, and person markup to home page.
*/
if ( false !== has_filter( $data_filter_name ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'applying filters "' . $data_filter_name . '"' );
}
$json_data = apply_filters( $data_filter_name, $json_data, $mod, $mt_og, $page_type_id, $is_main );
if ( false !== has_filter( $valid_filter_name ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'applying filters "' . $valid_filter_name . '"' );
}
$json_data = apply_filters( $valid_filter_name, $json_data, $mod, $mt_og, $page_type_id, $is_main );
}
/*
* Home page (static or blog archive).
*/
} elseif ( $mod[ 'is_home' ] && method_exists( $this, $method_filter_name ) ) {
/*
* $is_main is always false for methods.
*/
$json_data = call_user_func( array( $this, $method_filter_name ), $json_data, $mod, $mt_og, $page_type_id, false );
} else {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'data filter not found: ' . $data_filter_name );
}
}
}
if ( empty( $json_data ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'json data is empty' );
}
$json_data = array(); // Just in case.
} elseif ( isset( $json_data[ 0 ] ) && SucomUtil::is_non_assoc( $json_data ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'json data includes ' . count( $json_data ) . ' data arrays' );
}
} else {
self::update_data_id( $json_data, empty( $mod[ 'id' ] ) ? $page_type_id : array( $page_type_id, $mod[ 'id' ] ) );
$json_data = array( $json_data );
}
return $single ? reset( $json_data ) : $json_data;
}
public function get_json_data_home_website() {
$mod = WpssoAbstractWpMeta::get_mod_home();
$mt_og = array();
/*
* WpssoSchema->get_json_data() returns a two dimensional array of json data unless $single is true.
*/
$json_data = $this->get_json_data( $mod, $mt_og, $page_type_id = 'website', $is_main = false, $single = true );
return $json_data;
}
/*
* See WpssoFaqShortcodeFaq->do_shortcode().
* See WpssoFaqShortcodeQuestion->do_shortcode().
*/
public function get_mod_script_type_application_ld_json_html( array $mod, $css_id = '' ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
$json_data = $this->get_mod_json_data( $mod ); // Can return false.
if ( empty( $json_data ) ) { // Just in case.
return '';
}
WpssoSchemaGraph::clean_json( $json_data );
if ( empty( $css_id ) ) { // Just in case.
$css_id = 'wpsso-schema-' . md5( serialize( $json_data ) ); // md5() input must be a string.
}
return '<script type="application/ld+json" id="' . $css_id . '">' . $this->p->util->json_format( $json_data ) . '</script>' . "\n";
}
public function get_mod_json_data( array $mod ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
if ( empty( $mod[ 'name' ] ) || empty( $mod[ 'id' ] ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'exiting early: mod name or id is empty' );
}
return false;
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting schema type for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
}
$page_type_id = $this->get_mod_schema_type_id( $mod );
if ( empty( $page_type_id ) ) { // Just in case.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'exiting early: page type id is empty' );
}
return false;
} elseif ( 'none' === $page_type_id ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'exiting early: page type id is "none"' );
}
return false;
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'page type id is ' . $page_type_id );
}
$ref_url = $this->p->util->maybe_set_ref( null, $mod, __( 'adding schema', 'wpsso' ) );
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting open graph meta tag array' );
}
$mt_og = $this->p->og->get_array( $mod, $size_names = 'schema', $md_pre = array( 'schema', 'og' ) );
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting schema json-ld markup array' );
}
/*
* WpssoSchema->get_json_data() returns a two dimensional array of json data unless $single is true.
*/
$json_data = $this->get_json_data( $mod, $mt_og, $page_type_id, $is_main = true, $single = true );
$this->p->util->maybe_unset_ref( $ref_url );
return $json_data;
}
/*
* Returns the schema type id.
*/
public function get_mod_schema_type_id( array $mod, $use_md_opts = true ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
return $this->get_mod_schema_type( $mod, $get_id = true, $use_md_opts );
}
/*
* Returns the schema type id by default.
*
* Use $get_id = false to return the schema type URL instead of the ID.
*/
public function get_mod_schema_type( array $mod, $get_id = true, $use_md_opts = true ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
static $local_cache = array();
$cache_salt = false;
/*
* Archive pages can call this method several times.
*
* Optimize and cache post/term/user schema type values.
*/
if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
/*
* Note that the sort order, page number, locale, amp and embed checks are provided by
* WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
*/
$cache_salt = SucomUtil::get_mod_salt( $mod ) . '_get:' . (string) $get_id . '_use:' . (string) $use_md_opts;
if ( isset( $local_cache[ $cache_salt ] ) ) {
return $local_cache[ $cache_salt ];
}
}
$type_id = null;
$schema_types = $this->get_schema_types( $flatten = true );
/*
* Maybe get a custom schema type id from the post, term, or user meta.
*/
if ( $use_md_opts ) {
if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) { // Just in case.
$type_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'schema_type' ); // Returns null if an index key is not found.
if ( empty( $type_id ) || 'none' === $type_id || empty( $schema_types[ $type_id ] ) ) { // Check for an invalid type id.
$type_id = null;
} elseif ( $this->p->debug->enabled ) {
$this->p->debug->log( 'custom schema_type = ' . $type_id );
}
}
}
$is_custom = empty( $type_id ) ? false : true;
if ( ! $is_custom ) { // No custom schema type id from the post, term, or user meta.
/*
* Similar module type logic can be found in the following methods:
*
* See WpssoOpenGraph->get_mod_og_type().
* See WpssoPage->get_description().
* See WpssoPage->get_the_title().
* See WpssoSchema->get_mod_schema_type().
* See WpssoUtil->get_canonical_url().
*/
if ( $mod[ 'is_home' ] ) { // Home page (static or blog archive).
if ( $mod[ 'is_home_page' ] ) { // Static front page (singular post).
$type_id = $this->get_schema_type_id( 'home_page' );
} else $type_id = $this->get_schema_type_id( 'home_posts' );
} elseif ( $mod[ 'is_comment' ] ) {
if ( is_numeric( $mod[ 'comment_rating' ] ) ) {
$type_id = $this->get_schema_type_id( 'comment_review' );
} elseif ( $mod[ 'comment_parent' ] ) {
$type_id = $this->get_schema_type_id( 'comment_reply' );
} else $type_id = $this->get_schema_type_id( 'comment' );
} elseif ( $mod[ 'is_post' ] ) {
if ( $mod[ 'post_type' ] ) { // Just in case.
if ( $mod[ 'is_post_type_archive' ] ) { // The post ID may be 0.
$type_id = $this->get_schema_type_id( 'pta_' . $mod[ 'post_type' ] );
if ( empty( $type_id ) ) { // Just in case.
$type_id = $this->get_schema_type_id( 'archive_page' );
}
} else {
$type_id = $this->get_schema_type_id( $mod[ 'post_type' ] );
if ( empty( $type_id ) ) { // Just in case.
$type_id = $this->get_schema_type_id( 'page' );
}
}
} elseif ( $this->p->debug->enabled ) {
$this->p->debug->log( 'no post type' );
}
} elseif ( $mod[ 'is_term' ] ) {
if ( ! empty( $mod[ 'tax_slug' ] ) ) { // Just in case.
$type_id = $this->get_schema_type_id( 'tax_' . $mod[ 'tax_slug' ] );
}
if ( empty( $type_id ) ) { // Just in case.
$type_id = $this->get_schema_type_id( 'archive_page' );
}
} elseif ( $mod[ 'is_user' ] ) {
$type_id = $this->get_schema_type_id( 'user_page' );
} elseif ( $mod[ 'is_search' ] ) {
$type_id = $this->get_schema_type_id( 'search_page' );
} elseif ( $mod[ 'is_archive' ] ) {
$type_id = $this->get_schema_type_id( 'archive_page' );
}
if ( empty( $type_id ) ) { // Just in case.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'unable to determine schema type id (using default)' );
}
$type_id = 'webpage';
}
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'applying filters "wpsso_schema_type"' );
}
$type_id = apply_filters( 'wpsso_schema_type', $type_id, $mod, $is_custom );
$get_value = false;
if ( empty( $type_id ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning false: schema type id is empty' );
}
} elseif ( 'none' === $type_id ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning false: schema type id is disabled' );
}
} elseif ( ! isset( $schema_types[ $type_id ] ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning false: schema type id ' . $type_id . ' is unknown' );
}
} elseif ( ! $get_id ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning schema type url: ' . $schema_types[ $type_id ] );
}
$get_value = $schema_types[ $type_id ];
} else {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'returning schema type id: ' . $type_id );
}
$get_value = $type_id;
}
/*
* Optimize and cache post/term/user schema type values.
*/
if ( $cache_salt ) {
$local_cache[ $cache_salt ] = $get_value;
}
return $get_value;
}
/*
* Returns the schema type URL.
*/
public function get_mod_schema_type_url( array $mod, $use_md_opts = true ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
return $this->get_mod_schema_type( $mod, $get_id = false, $use_md_opts );
}
/*
* Refresh the Schema types transient cache.
*/
public function refresh_schema_types() {
$this->get_schema_types( $flatten = true, $read_cache = false );
self::get_schema_type_row_class( $name = 'schema_type', $read_cache = false );
self::get_schema_type_row_class( $name = 'schema_review_item_type', $read_cache = false );
}
public static function get_schema_product_price_valid_date() {
$wpsso =& Wpsso::get_instance();
static $valid_date = null;
if ( null === $valid_date ) {
if ( ! empty( $wpsso->options[ 'schema_def_product_price_valid_days' ] ) ) {
$valid_date = gmdate( 'c', time() + ( $wpsso->options[ 'schema_def_product_price_valid_days' ] * DAY_IN_SECONDS ) );
}
}
return $valid_date;
}
/*
* Returns a one-dimensional (flat) array of schema types by default, otherwise returns a multi-dimensional array
* of all schema types, including cross-references for sub-types with multiple parent types.
*
* $read_cache is false when called from the WpssoUpgrade::options() method.
*
* Uses a transient cache object and the $types_cache class property.
*/
public function get_schema_types( $flatten = true, $read_cache = true ) {
if ( ! $read_cache ) {
$this->types_cache = array();
}
if ( ! isset( $this->types_cache[ 'filtered' ] ) ) {
$cache_md5_pre = 'wpsso_t_';
$cache_exp_secs = $this->p->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient' );
if ( $cache_exp_secs > 0 ) {
$cache_salt = __METHOD__;
$cache_id = $cache_md5_pre . md5( $cache_salt );
if ( $read_cache ) {
$this->types_cache = get_transient( $cache_id ); // Returns false when not found.
if ( ! empty( $this->types_cache ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'using schema types array from transient ' . $cache_id );
}
} else $this->types_cache = array();
}
}
if ( ! isset( $this->types_cache[ 'filtered' ] ) ) { // Maybe from transient cache - re-check if filtered.
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'create schema types array' ); // Begin timer.
}
/*
* Filtered array.
*/
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'applying filters "wpsso_schema_types"' );
}
$this->types_cache[ 'filtered' ] = apply_filters( 'wpsso_schema_types', $this->p->cf[ 'head' ][ 'schema_type' ] );
/*
* Flattened array (before adding cross-references).
*/
$this->types_cache[ 'flattened' ] = SucomUtil::array_flatten( $this->types_cache[ 'filtered' ] );
/*
* Adding cross-references to filtered array.
*/
$this->add_schema_type_xrefs( $this->types_cache[ 'filtered' ] );
/*
* Parents array.
*/
$this->types_cache[ 'parents' ] = SucomUtil::get_array_parents( $this->types_cache[ 'filtered' ] );
if ( $cache_exp_secs > 0 ) {
set_transient( $cache_id, $this->types_cache, $cache_exp_secs );
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'schema types array saved to transient cache for ' . $cache_exp_secs . ' seconds' );
}
}
if ( $this->p->debug->enabled ) {
$this->p->debug->mark( 'create schema types array' ); // End timer.
}
} elseif ( $this->p->debug->enabled ) {
$this->p->debug->log( 'schema types array already filtered' );
}
}
if ( $flatten ) {
return $this->types_cache[ 'flattened' ];
}
return $this->types_cache[ 'filtered' ];
}
public function get_schema_types_select( $schema_types = null ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
if ( ! is_array( $schema_types ) ) {
$schema_types = $this->get_schema_types( $flatten = false );
}
$schema_types = SucomUtil::array_flatten( $schema_types );
$select = array();
foreach ( $schema_types as $type_id => $type_url ) {
list( $type_context, $type_name, $type_path ) = $this->get_schema_type_url_parts( $type_url );
switch ( $this->p->options[ 'plugin_schema_types_select_format' ] ) {
case 'name': // Options default.
$select[ $type_id ] = $type_name;
break;
case 'name_id':
$select[ $type_id ] = $type_name . ' [' . $type_id . ']';
break;
case 'id':
$select[ $type_id ] = $type_id;
break;
case 'id_url':
$select[ $type_id ] = $type_id . ' | ' . $type_path;
break;
case 'id_name':
$select[ $type_id ] = $type_id . ' | ' . $type_name;
break;
default:
$select[ $type_id ] = $type_name;
break;
}
}
if ( defined( 'SORT_STRING' ) ) { // Just in case.
asort( $select, SORT_STRING );
} else {
asort( $select );
}
return $select;
}
/*
* Returns an array of schema type ids with gparent, parent, child (in that order).
*
* $use_cache is false when calling get_schema_type_child_family() recursively.
*/
public function get_schema_type_child_family( $child_id, $use_cache = true, &$child_family = array() ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
if ( $use_cache ) {
$cache_md5_pre = 'wpsso_t_';
$cache_exp_secs = $this->p->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient' );
if ( $cache_exp_secs > 0 ) {
$cache_salt = __METHOD__ . '(child_id:' . $child_id . ')';
$cache_id = $cache_md5_pre . md5( $cache_salt );
$child_family = get_transient( $cache_id ); // Returns false when not found.
if ( is_array( $child_family ) ) {
return $child_family;
} else $child_family = array();
}
}
$schema_types = $this->get_schema_types( $flatten = true ); // Defines the 'parents' array.
if ( isset( $this->types_cache[ 'parents' ][ $child_id ] ) ) {
foreach( $this->types_cache[ 'parents' ][ $child_id ] as $parent_id ) {
if ( $parent_id !== $child_id ) { // Prevent infinite loops.
/*
* $use_cache is false for recursive calls.
*/
$this->get_schema_type_child_family( $parent_id, $child_use_cache = false, $child_family );
}
}
}
$child_family[] = $child_id; // Add child after parents.
$child_family = array_unique( $child_family );
if ( $use_cache ) {
if ( $cache_exp_secs > 0 ) {
set_transient( $cache_id, $child_family, $cache_exp_secs );
}
}
return $child_family;
}
/*
* Returns an array of schema type ids with child, parent, gparent (in that order).
*
* $use_cache is false when calling get_schema_type_children() recursively.
*/
public function get_schema_type_children( $type_id, $use_cache = true, &$children = array() ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting children for type id ' . $type_id );
}
if ( $use_cache ) {
$cache_md5_pre = 'wpsso_t_';
$cache_exp_secs = $this->p->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient' );
if ( $cache_exp_secs > 0 ) {
$cache_salt = __METHOD__ . '(type_id:' . $type_id . ')';
$cache_id = $cache_md5_pre . md5( $cache_salt );
$children = get_transient( $cache_id ); // Returns false when not found.
if ( is_array( $children ) ) {
return $children;
} else $children = array();
}
}
$children[] = $type_id; // Add children before parents.
$schema_types = $this->get_schema_types( $flatten = true ); // Defines the 'parents' array.
if ( isset( $this->types_cache[ 'parents' ] ) && is_array( $this->types_cache[ 'parents' ] ) ) {
foreach ( $this->types_cache[ 'parents' ] as $child_id => $parent_ids ) {
foreach( $parent_ids as $parent_id ) {
if ( $parent_id === $type_id ) {
/*
* $use_cache is false for recursive calls.
*/
$this->get_schema_type_children( $child_id, $child_use_cache = false, $children );
}
}
}
}
$children = array_unique( $children );
if ( $use_cache ) {
if ( $cache_exp_secs > 0 ) {
set_transient( $cache_id, $children, $cache_exp_secs );
}
}
return $children;
}
public static function add_schema_type_context( $type_url, &$json_data = array() ) {
$json_data = self::get_schema_type_context( $type_url, $json_data );
}
public static function get_schema_type_context( $type_url, $json_data = array() ) {
if ( preg_match( '/^(.+:\/\/.+)\/([^\/]+)$/', $type_url, $match ) ) {
$context_value = $match[ 1 ];
$type_value = $match[ 2 ];
/*
* Check for schema extension (example: https://health-lifesci.schema.org).
*
* $context_value = array(
* "https://schema.org",
* array(
* "health-lifesci" => "https://health-lifesci.schema.org",
* ),
* );
*
*/
if ( preg_match( '/^(.+:\/\/)([^\.]+)\.([^\.]+\.[^\.]+)$/', $context_value, $ext ) ) {
$context_value = array(
$ext[ 1 ] . $ext[ 3 ],
array( $ext[ 2 ] => $ext[ 0 ] )
);
}
$json_head = array(
'@id' => null,
'@context' => null,
'@type' => null,
);
$json_values = array(
'@context' => $context_value,
'@type' => $type_value,
);
if ( is_array( $json_data ) ) { // Just in case.
/*
* Include $json_head first to keep @id, @context, and @type top-most.
*/
$json_data = array_merge( $json_head, $json_data, $json_values );
if ( empty( $json_data[ '@id' ] ) ) {
unset( $json_data[ '@id' ] );
}
} else return $json_values;
}
return $json_data;
}
/*
* See WpssoIntegEcomWooCommerce->filter_schema_type().
* See WpssoSchema->get_mod_schema_type().
*/
public function get_schema_type_id( $opt_suffix, $default_id = false, $use_defs = false ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log_args( array(
'opt_suffix' => $opt_suffix,
'default_id' => $default_id,
) );
}
if ( empty( $opt_suffix ) ) { // Just in case.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'exiting early: opt_suffix is empty' );
}
return $default_id;
} elseif ( ! is_string( $opt_suffix ) ) { // Must be a string.
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'exiting early: opt_suffix is ' . gettype( $opt_suffix ) );
}
return $default_id;
}
$schema_types = $this->get_schema_types( $flatten = true ); // Uses a class variable cache.
if ( empty( $default_id ) ) { // $default_id is false or empty string, and $opt_suffix is a string.
$default_id = SucomUtil::sanitize_schema_id( $opt_suffix );
if ( empty( $schema_types[ $default_id ] ) ) $default_id = false;
}
$opt_key = SucomUtil::sanitize_key( 'schema_type_for_' . $opt_suffix );
if ( $use_defs ) {
$type_id = isset( $this->p->defaults[ $opt_key ] ) ? $this->p->defaults[ $opt_key ] : $default_id;
} else $type_id = isset( $this->p->options[ $opt_key ] ) ? $this->p->options[ $opt_key ] : $default_id;
if ( empty( $type_id ) || 'none' === $type_id || empty( $schema_types[ $type_id ] ) ) { // Invalid type id.
if ( empty( $default_id ) || 'none' === $default_id || empty( $schema_types[ $default_id ] ) ) { // Invalid default id.
return false;
}
return $default_id;
}
return $type_id;
}
/*
* See WpssoMessagesTooltipSchema->get().
*/
public function get_default_schema_type_name( $opt_suffix ) {
$type_id = $this->get_schema_type_id( $opt_suffix, $default_id = false, $use_defs = true );
list( $type_context, $type_name, $type_path ) = $this->get_schema_type_url_parts_by_id( $type_id );
return $type_name;
}
/*
* Check if the Schema type matches a pre-defined Open Graph type.
*
* For example, a Schema place sub-type would return 'place' for the Open Graph type.
*
* Returns false or an Open Graph type string.
*/
public function get_schema_type_og_type( $type_id ) {
static $local_cache = array(); // Cache for single page load.
if ( isset( $local_cache[ $type_id ] ) ) {
return $local_cache[ $type_id ];
}
/*
* Hard-code the Open Graph type based on the Schema type.
*/
foreach ( $this->p->cf[ 'head' ][ 'og_type_by_schema_type' ] as $parent_id => $og_type ) {
if ( $this->is_schema_type_child( $type_id, $parent_id ) ) {
return $local_cache[ $type_id ] = $og_type;
}
}
return $local_cache[ $type_id ] = false;
}
public function ajax_schema_type_og_type() {
$doing_ajax = SucomUtilWP::doing_ajax();
if ( ! $doing_ajax ) { // Just in case.
return;
} elseif ( SucomUtil::get_const( 'DOING_AUTOSAVE' ) ) {
die( -1 );
}
check_ajax_referer( WPSSO_NONCE_NAME, '_ajax_nonce', $die = true );
$schema_type = sanitize_text_field( filter_input( INPUT_POST, 'schema_type' ) );
if ( $og_type = $this->get_schema_type_og_type( $schema_type ) ) {
die( $og_type );
} else {
die( -1 );
}
}
/*
* Javascript classes to hide/show table rows by the selected schema type value.
*
* See WpssoSchema->refresh_schema_types().
*/
public static function get_schema_type_row_class( $name = 'schema_type', $read_cache = true ) {
if ( is_admin() ) {
static $local_cache = array();
if ( $read_cache ) {
if ( isset( $local_cache[ $name ] ) ) {
return $local_cache[ $name ];
}
}
} else $local_cache = array();
$wpsso =& Wpsso::get_instance();
$cache_md5_pre = 'wpsso_t_';
$cache_exp_secs = $wpsso->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient' );
if ( $cache_exp_secs > 0 ) {
$opt_version = $wpsso->opt->get_version( $wpsso->options, 'wpsso' ); // Returns 'opt_version'.
$cache_salt = __METHOD__ . '(opt_version:' . $opt_version . ')';
$cache_id = $cache_md5_pre . md5( $cache_salt );
if ( $read_cache ) {
$local_cache = get_transient( $cache_id ); // Returns false when not found.
if ( is_array( $local_cache ) ) {
if ( isset( $local_cache[ $name ] ) ) {
return $local_cache[ $name ];
}
} else $local_cache = array();
}
}
$type_ids = array();
switch ( $name ) {
case 'schema_type':
$type_ids = array(
'article',
'book',
'book.audio',
'course',
'creative.work',
'event',
'howto',
'item.list',
'job.posting',
'local.business',
'learning.resource',
'movie',
'organization',
'person',
'place',
'product',
'question',
'recipe',
'review',
'review.claim',
'service',
'software.application',
'webpage',
'webpage.faq',
'webpage.profile',
'webpage.qa',
);
break;
case 'schema_review_item_type':
$type_ids = array(
'book',
'creative.work',
'food.establishment',
'local.business',
'movie',
'place',
'product',
'software.application',
);
break;
}
foreach ( $type_ids as $type_id ) {
switch ( $type_id ) {
case 'howto':
$exclude_match = '/^recipe$/';
break;
default:
$exclude_match = '';
break;
}
$local_cache[ $name ][ $type_id ] = $wpsso->schema->get_children_css_class( $type_id, $class_prefix = 'hide_' . $name, $exclude_match );
}
unset( $type_ids, $type_id, $exclude_match );
if ( $cache_exp_secs > 0 ) {
set_transient( $cache_id, $local_cache, $cache_exp_secs );
}
return $local_cache[ $name ];
}
/*
* Get the full schema type url from the array key.
*
* Returns false if the Schema type id not found.
*/
public function get_schema_type_url( $type_id, $default_id = false ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'getting schema url for ' . $type_id );
}
if ( ! empty( $type_id ) ) { // Not null, false, 0, or empty string.
$schema_types = $this->get_schema_types( $flatten = true );
if ( 'none' !== $type_id && isset( $schema_types[ $type_id ] ) ) {
return $schema_types[ $type_id ];
} elseif ( false !== $default_id && isset( $schema_types[ $default_id ] ) ) {
return $schema_types[ $default_id ];
}
}
return false;
}
/*
* Returns an array of schema type id for a given type URL.
*/
public function get_schema_type_url_ids( $type_url ) {
$type_ids = array();
$schema_types = $this->get_schema_types( $flatten = true );
foreach ( $schema_types as $id => $url ) {
if ( $url === $type_url ) {
$type_ids[] = $id;
}
}
return $type_ids;
}
/*
* Returns the first schema type id for a given type URL.
*/
public function get_schema_type_url_id( $type_url, $default_id = false ) {
$schema_types = $this->get_schema_types( $flatten = true );
foreach ( $schema_types as $id => $url ) {
if ( $url === $type_url ) {
return $id;
}
}
return $default_id;
}
/*
* Returns an array with the Schema type context, name, and URL (without protocol prefix).
*
* Example array( 'https://schema.org', 'TechArticle', 'schema.org/TechArticle' ).
*
* Returns array( null, null, null ) if the Schema type URL is invalid.
*/
public function get_schema_type_url_parts( $type_url ) {
if ( is_string( $type_url ) ) { // Just in case.
if ( preg_match( '/^(.+:\/\/.+)\/(.+)$/', $type_url, $match ) ) {
$type_path = preg_replace( '/^.*\/\//', '', $type_url ); // Remove 'https://'.
return array( $match[ 1 ], $match[ 2 ], $type_path );
}
}
return array( null, null, null );
}
public function get_schema_type_url_parts_by_id( $type_id ) {
$type_url = $this->get_schema_type_url( $type_id ); // Returns false if the Schema type id not found.
return $this->get_schema_type_url_parts( $type_url ); // Returns array( null, null, null ) if the Schema type URL is invalid.
}
public function get_children_css_class( $type_id, $class_prefix = 'hide_schema_type', $exclude_match = '' ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->mark();
}
if ( empty( $class_prefix ) ) {
$css_classes = '';
$class_prefix = '';
} else {
$css_classes = $class_prefix;
$class_prefix = SucomUtil::sanitize_css_id( $class_prefix ) . '_';
}
foreach ( $this->get_schema_type_children( $type_id ) as $child ) {
if ( ! empty( $exclude_match ) ) {
if ( preg_match( $exclude_match, $child ) ) {
continue;
}
}
$css_classes .= ' ' . $class_prefix . SucomUtil::sanitize_css_id( $child );
}
$css_classes = trim( $css_classes );
return $css_classes;
}
public function is_schema_type_child( $child_id, $member_id ) {
static $local_cache = array(); // Cache for single page load.
if ( isset( $local_cache[ $child_id ][ $member_id ] ) ) {
return $local_cache[ $child_id ][ $member_id ];
}
if ( $child_id === $member_id ) { // Optimize and check for obvious.
$is_child = true;
} else {
$child_family = $this->get_schema_type_child_family( $child_id );
$is_child = in_array( $member_id, $child_family ) ? true : false;
}
return $local_cache[ $child_id ][ $member_id ] = $is_child;
}
public function count_schema_type_children( $type_id ) {
$children = $this->get_schema_type_children( $type_id );
return count( $children );
}
public function has_json_data_filter( array $mod, $type_url = '' ) {
$filter_name = $this->get_json_data_filter( $mod, $type_url );
return empty( $filter_name ) ? false : has_filter( $filter_name );
}
public function get_json_data_filter( array $mod, $type_url = '' ) {
if ( empty( $type_url ) ) {
$type_url = $this->get_mod_schema_type_url( $mod );
}
return 'wpsso_json_data_' . SucomUtil::sanitize_hookname( $type_url );
}
/*
* Since WPSSO Core v9.2.1.
*
* Check if Google allows aggregate rarings for this Schema type.
*/
public function allow_aggregate_rating( $page_type_id ) {
foreach ( $this->p->cf[ 'head' ][ 'schema_aggregate_rating_parents' ] as $parent_id ) {
if ( $this->is_schema_type_child( $page_type_id, $parent_id ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'aggregate rating for schema type ' . $page_type_id . ' is allowed' );
}
return true;
}
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'aggregate rating for schema type ' . $page_type_id . ' not allowed' );
}
return false;
}
/*
* Since WPSSO Core v9.2.1.
*
* Check if Google allows reviews for this Schema type.
*/
public function allow_review( $page_type_id ) {
foreach ( $this->p->cf[ 'head' ][ 'schema_review_parents' ] as $parent_id ) {
if ( $this->is_schema_type_child( $page_type_id, $parent_id ) ) {
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'review for schema type ' . $page_type_id . ' is allowed' );
}
return true;
}
}
if ( $this->p->debug->enabled ) {
$this->p->debug->log( 'review for schema type ' . $page_type_id . ' not allowed' );
}
return false;
}
/*
* json_data can be null, so don't cast an array on the input argument.
*
* The @context value can be an array if the schema type is an extension.
*
* @context = array(
* "https://schema.org",
* array(
* "health-lifesci" => "https://health-lifesci.schema.org",
* )
* )
*/
public static function get_data_type_id( $json_data, $default_id = false ) {
$wpsso =& Wpsso::get_instance();
$type_url = self::get_data_type_url( $json_data );
return $wpsso->schema->get_schema_type_url_id( $type_url, $default_id );
}
public static function get_data_type_url( $json_data ) {
$type_url = false;
if ( empty( $json_data[ '@type' ] ) ) {
return false; // Stop here.
} elseif ( is_array( $json_data[ '@type' ] ) ) {
$json_data[ '@type' ] = reset( $json_data[ '@type' ] ); // Use first @type element.
$type_url = self::get_data_type_url( $json_data );
} elseif ( strpos( $json_data[ '@type' ], '://' ) ) { // @type is a complete url
$type_url = $json_data[ '@type' ];
} elseif ( ! empty( $json_data[ '@context' ] ) ) { // Just in case.
if ( is_array( $json_data[ '@context' ] ) ) { // Get the extension url.
$context_url = self::get_context_extension_url( $json_data[ '@context' ] );
if ( ! empty( $context_url ) ) { // Just in case.
$type_url = trailingslashit( $context_url ) . $json_data[ '@type' ];
}
} elseif ( is_string( $json_data[ '@context' ] ) ) {
$type_url = trailingslashit( $json_data[ '@context' ] ) . $json_data[ '@type' ];
}
}
$type_url = set_url_scheme( $type_url, 'https' ); // Just in case.
return $type_url;
}
/*
* Returns array() if no schema type found.
*/
public static function get_data_context( $json_data ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( false !== ( $type_url = self::get_data_type_url( $json_data ) ) ) {
return self::get_schema_type_context( $type_url, SucomUtil::preg_grep_keys( '/^@/', $json_data ) );
}
return array();
}
public static function get_context_extension_url( array $json_data ) {
$type_url = false;
$ext_data = array_reverse( $json_data ); // Read the array bottom-up.
foreach ( $ext_data as $val ) {
if ( is_array( $val ) ) { // If it's an extension array, drill down and return that value.
return self::get_context_extension_url( $val );
} elseif ( is_string( $val ) ) { // Set a backup value in case there is no extension array.
$type_url = $val;
}
}
return false;
}
/*
* Get the site organization array.
*
* $mixed = 'default' | 'current' | post ID | $mod array
*/
public static function get_site_organization( $mixed = 'current' ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$org_opts = array(
'org_url' => SucomUtilWP::get_home_url( $wpsso->options, $mixed ),
'org_name' => SucomUtilWP::get_site_name( $wpsso->options, $mixed ),
'org_name_alt' => SucomUtilWP::get_site_name_alt( $wpsso->options, $mixed ),
'org_desc' => SucomUtilWP::get_site_description( $wpsso->options, $mixed ),
'org_place_id' => $wpsso->options[ 'site_org_place_id' ],
'org_schema_type' => $wpsso->options[ 'site_org_schema_type' ],
);
/*
* Add localized option values.
*
* Example 'site_org_logo_url:width#fr_FR'.
*/
foreach ( array(
'org_logo_url',
'org_logo_url:width',
'org_logo_url:height',
'org_banner_url',
'org_banner_url:width',
'org_banner_url:height',
) as $opt_key ) {
$org_opts[ $opt_key ] = SucomUtilOptions::get_key_value( 'site_' . $opt_key, $wpsso->options, $mixed );
}
/*
* Add sameas option values.
*/
$org_opts[ 'org_sameas' ] = array();
foreach ( WpssoConfig::get_social_accounts() as $social_key => $social_label ) {
$url = SucomUtilOptions::get_key_value( $social_key, $wpsso->options, $mixed ); // Localized value.
if ( empty( $url ) ) { // Nothing to do.
continue;
} elseif ( $social_key === 'tc_site' ) { // Convert X (Twitter) username to a URL.
$url = 'https://twitter.com/' . preg_replace( '/^@/', '', $url );
}
if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping ' . $social_key . ': url "' . $url . '" is invalid' );
}
} else {
$org_opts[ 'org_sameas' ][] = $url;
}
}
return $org_opts;
}
public static function add_author_coauthors_data( &$json_data, $mod, $user_id = false ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$authors_added = 0;
$coauthors_added = 0;
if ( empty( $user_id ) && isset( $mod[ 'post_author' ] ) ) {
$user_id = $mod[ 'post_author' ];
}
if ( empty( $user_id ) || 'none' === $user_id ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: user id is empty or "none"' );
}
return 0; // Return count of authors and coauthors added.
}
/*
* Single author.
*/
$authors_added += WpssoSchemaSingle::add_person_data( $json_data[ 'author' ], $mod, $user_id, $list_el = false );
/*
* List of contributors / co-authors.
*/
if ( ! empty( $mod[ 'post_coauthors' ] ) ) {
foreach ( $mod[ 'post_coauthors' ] as $author_id ) {
$coauthors_added += WpssoSchemaSingle::add_person_data( $json_data[ 'contributor' ], $mod, $author_id, $list_el = true );
}
}
return $authors_added + $coauthors_added; // Return count of authors and coauthors added.
}
/*
* $user_id is optional and takes precedence over the $mod post_author value.
*/
public static function add_comments_data( &$json_data, $post_mod ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$comments_added = 0;
if ( ! $post_mod[ 'is_post' ] || ! $post_mod[ 'id' ] || ! comments_open( $post_mod[ 'id' ] ) ) {
return $comments_added;
}
$json_data[ 'commentCount' ] = (int) get_comments_number( $post_mod[ 'id' ] );
/*
* Get parent comments.
*
* The WpssoSchemaSingle::add_comment_data() method will recurse and add the replies (ie. children).
*/
if ( get_option( 'page_comments' ) ) { // "Break comments into pages" option is checked.
$comment_order = strtoupper( get_option( 'comment_order' ) );
$comment_paged = $post_mod[ 'comment_paged' ] ? $post_mod[ 'comment_paged' ] : 1; // Get the comment page number.
$comment_number = get_option( 'comments_per_page' );
} else {
$comment_order = 'DESC';
$comment_paged = 1;
$comment_number = SucomUtil::get_const( 'WPSSO_SCHEMA_COMMENTS_MAX' );
}
if ( $comment_number ) { // 0 disables the addition of comments.
$get_comment_args = array(
'post_id' => $post_mod[ 'id' ],
'status' => 'approve',
'parent' => 0, // Don't get replies.
'order' => $comment_order,
'orderby' => 'comment_date_gmt',
'paged' => $comment_paged,
'number' => $comment_number,
);
$comments = get_comments( $get_comment_args );
if ( is_array( $comments ) ) {
foreach( $comments as $num => $comment_obj ) {
$comments_added += WpssoSchemaSingle::add_comment_data( $json_data[ 'comment' ], $post_mod, $comment_obj->comment_ID );
}
}
}
return $comments_added; // Return count of comments added.
}
/*
* See WpssoJsonTypeHowTo->filter_json_data_https_schema_org_howto().
* See WpssoJsonTypeRecipe->filter_json_data_https_schema_org_recipe().
*/
public static function add_howto_steps_data( &$json_data, $mod, $md_opts, $opt_pre = 'schema_howto_step', $prop_name = 'step' ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$canonical_url = $wpsso->util->get_canonical_url( $mod );
/*
* Get the howto step names.
*/
$steps = SucomUtil::preg_grep_keys( '/^' . $opt_pre . '_([0-9]+)$/', $md_opts, $invert = false, $replace = '$1' );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log_arr( 'steps', $steps );
}
if ( ! empty( $steps ) ) { // Just in case.
$section_ref = false;
$step_pos = 0;
$step_idx = 0;
/*
* $md_val is the howto section/step name.
*/
foreach ( $steps as $num => $md_val ) {
/*
* Maybe get a longer text / description value.
*/
$step_text = isset( $md_opts[ $opt_pre . '_text_' . $num ] ) ? $md_opts[ $opt_pre . '_text_' . $num ] : $md_val;
if ( empty( $md_val ) && empty( $step_text ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping step ' . $num . ': step name and text are empty' );
}
}
/*
* Get the section or step anchored URL, if an anchor ID has been provided.
*/
$step_anchor_id = empty( $md_opts[ $opt_pre . '_anchor_id_' . $num ] ) ? '' : $md_opts[ $opt_pre . '_anchor_id_' . $num ];
$step_url = empty( $step_anchor_id ) ? '' : SucomUtil::append_url_fragment( $canonical_url, $step_anchor_id );
/*
* Get images for the section or step.
*/
$step_images = array();
if ( ! empty( $md_opts[ $opt_pre . '_img_id_' . $num ] ) ) {
/*
* Set reference values for admin notices.
*/
if ( is_admin() ) {
$wpsso->notice->set_ref( $canonical_url, $mod, sprintf( __( 'adding schema %s #%d image', 'wpsso' ),
$prop_name, $num + 1 ) );
}
/*
* $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
*/
$mt_images = $wpsso->media->get_mt_opts_images( $md_opts, $size_names = 'schema', $opt_pre . '_img', $num );
self::add_images_data_mt( $step_images, $mt_images );
/*
* Restore previous reference values for admin notices.
*/
if ( is_admin() ) {
$wpsso->notice->unset_ref( $canonical_url );
}
}
/*
* Add a How-To Section.
*/
if ( ! empty( $md_opts[ $opt_pre . '_section_' . $num ] ) ) {
$json_data[ $prop_name ][ $step_idx ] = self::get_schema_type_context( 'https://schema.org/HowToSection',
array(
'url' => $step_url,
'name' => $md_val, // Section name.
'description' => $step_text,
'numberOfItems' => 0,
'itemListOrder' => 'https://schema.org/ItemListOrderAscending',
'itemListElement' => array(),
)
);
if ( $step_images ) {
$json_data[ $prop_name ][ $step_idx ][ 'image' ] = $step_images;
}
$section_ref =& $json_data[ $prop_name ][ $step_idx ];
$step_pos = 0;
$step_idx++;
/*
* Add a How-To Step.
*/
} else {
$step_pos++;
$step_data = self::get_schema_type_context( 'https://schema.org/HowToStep',
array(
'position' => $step_pos,
'url' => $step_url,
'name' => $md_val, // Step name.
'text' => $step_text,
'image' => null,
)
);
if ( ! empty( $step_images ) ) {
$step_data[ 'image' ] = $step_images;
}
/*
* If we have a section, add a new step to the section.
*/
if ( false !== $section_ref ) {
$section_ref[ 'itemListElement' ][] = $step_data;
$section_ref[ 'numberOfItems' ] = $step_pos;
} else {
$json_data[ $prop_name ][ $step_idx ] = $step_data;
$step_idx++;
}
}
}
}
}
/*
* Pass a single or two dimension image array in $mt_images.
*
* Calls WpssoSchemaSingle::add_image_data_mt() to add each single image element.
*/
public static function add_images_data_mt( &$json_data, $mt_images, $media_pre = 'og:image', $resize = false ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$img_added = 0;
if ( empty( $mt_images ) || ! is_array( $mt_images ) ) {
return $img_added; // Return count of images added.
}
/*
* Maybe convert single image array to array of image arrays.
*/
if ( ! isset( $mt_images[ 0 ] ) || ! is_array( $mt_images[ 0 ] ) ) {
$mt_images = array( $mt_images );
}
$resized_pids = array(); // Avoid adding the same image ID more than once.
foreach ( $mt_images as $mt_single_image ) {
/*
* Get the image ID and create a Schema images array.
*/
if ( $resize && $pid = SucomUtil::get_first_mt_media_id( $mt_single_image, $media_pre ) ) {
if ( empty( $resized_pids[ $pid ] ) ) { // Skip image IDs already added.
$resized_pids[ $pid ] = true;
$mt_resized = $wpsso->media->get_mt_pid_images( $pid, $size_names = 'schema', $mt_pid_pre = 'og' );
/*
* Recurse this method, but make sure $resize is false so we don't re-execute this
* section of code (creating an infinite loop).
*/
$img_added += self::add_images_data_mt( $json_data, $mt_resized, $media_pre, $resize = false );
}
} else { // No resize or no image ID found.
$img_added += WpssoSchemaSingle::add_image_data_mt( $json_data, $mt_single_image, $media_pre, $list_el = true );
}
}
return $img_added; // Return count of images added.
}
public static function add_item_reviewed_data( &$json_data, $mod, $md_opts ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( self::is_valid_key( $md_opts, 'schema_review_item_type' ) ) { // Not null, an empty string, or 'none'.
$type_id = $md_opts[ 'schema_review_item_type' ];
} else {
$type_id = 'thing';
}
$type_url = $wpsso->schema->get_schema_type_url( $type_id ); // Returns false if the Schema type id not found.
if ( ! $wpsso->schema->allow_review( $type_id ) ) {
list( $type_context, $type_name, $type_path ) = $wpsso->schema->get_schema_type_url_parts( $type_url );
$notice_msg = sprintf( __( 'Please note that although the Schema standard allows the subject of a review to be any Schema type, <a href="%1$s">Google does not allow reviews for the Schema %2$s type</a>.', 'wpsso' ), 'https://developers.google.com/search/docs/data-types/review-snippet', $type_name ) . ' ';
$wpsso->notice->warn( $notice_msg );
}
$json_data = self::get_schema_type_context( $type_url, $json_data );
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'url' => 'schema_review_item_url',
'name' => 'schema_review_item_name',
'description' => 'schema_review_item_desc',
) );
foreach ( SucomUtil::get_multi_values( $md_opts, 'schema_review_item_sameas_url' ) as $url ) {
$json_data[ 'sameAs' ][] = SucomUtil::esc_url_encode( $url );
}
self::check_prop_value_sameas( $json_data );
/*
* Set reference values for admin notices.
*/
if ( is_admin() ) {
$canonical_url = $wpsso->util->get_canonical_url( $mod );
$wpsso->util->maybe_set_ref( $canonical_url, $mod, __( 'adding reviewed subject image', 'wpsso' ) );
}
/*
* Add the item images.
*
* $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
*/
$mt_images = $wpsso->media->get_mt_opts_images( $md_opts, $size_names = 'schema', $img_prefix = 'schema_review_item_img' );
self::add_images_data_mt( $json_data[ 'image' ], $mt_images );
if ( empty( $json_data[ 'image' ] ) ) {
unset( $json_data[ 'image' ] ); // Prevent null assignment.
} elseif ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $json_data[ 'image' ] );
}
/*
* Restore previous reference values for admin notices.
*/
if ( is_admin() ) {
$wpsso->util->maybe_unset_ref( $canonical_url );
}
/*
* Item Reviewed: Creative Work
*/
if ( $wpsso->schema->is_schema_type_child( $type_id, 'creative.work' ) ) {
/*
* The author type value should be either 'organization' or 'person'.
*/
if ( self::is_valid_key( $md_opts, 'schema_review_item_cw_author_type' ) ) { // Not null, an empty string, or 'none'.
$author_type_url = $wpsso->schema->get_schema_type_url( $md_opts[ 'schema_review_item_cw_author_type' ] );
$json_data[ 'author' ] = self::get_schema_type_context( $author_type_url );
self::add_data_itemprop_from_assoc( $json_data[ 'author' ], $md_opts, array(
'name' => 'schema_review_item_cw_author_name',
) );
if ( ! empty( $md_opts[ 'schema_review_item_cw_author_url' ] ) ) {
$json_data[ 'author' ][ 'sameAs' ][] = SucomUtil::esc_url_encode( $md_opts[ 'schema_review_item_cw_author_url' ] );
}
}
/*
* Subject Published Date.
*
* Add the creative work published date, if one is available.
*/
if ( $date = self::get_opts_date_iso( $md_opts, 'schema_review_item_cw_pub' ) ) {
$json_data[ 'datePublished' ] = $date;
}
/*
* Subject Created Date.
*
* Add the creative work created date, if one is available.
*/
if ( $date = self::get_opts_date_iso( $md_opts, 'schema_review_item_cw_created' ) ) {
$json_data[ 'dateCreated' ] = $date;
}
/*
* Item Reviewed: Creative Work > Book
*/
if ( $wpsso->schema->is_schema_type_child( $type_id, 'book' ) ) {
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'isbn' => 'schema_review_item_cw_book_isbn',
) );
/*
* Item Reviewed: Creative Work > Movie
*/
} elseif ( $wpsso->schema->is_schema_type_child( $type_id, 'movie' ) ) {
/*
* Property:
* actor (supersedes actors)
*/
self::add_person_names_data( $json_data, $prop_name = 'actor', $md_opts, 'schema_review_item_cw_movie_actor_person_name' );
/*
* Property:
* director
*/
self::add_person_names_data( $json_data, $prop_name = 'director', $md_opts, 'schema_review_item_cw_movie_director_person_name' );
/*
* Item Reviewed: Creative Work > Software Application
*/
} elseif ( $wpsso->schema->is_schema_type_child( $type_id, 'software.application' ) ) {
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'applicationCategory' => 'schema_review_item_software_app_cat',
'operatingSystem' => 'schema_review_item_software_app_os',
'downloadUrl' => 'schema_review_item_software_app_dl_url',
) );
$md_offers_max = SucomUtil::get_const( 'WPSSO_SCHEMA_METADATA_OFFERS_MAX' );
foreach ( range( 0, $md_offers_max - 1, 1 ) as $key_num ) {
$offer_opts = SucomUtil::preg_grep_keys( '/^schema_review_item_software_app_(offer_.*)_' . $key_num. '$/',
$md_opts, $invert = false, $replace = '$1' );
/*
* Must have at least an offer name and price.
*
* Values cannot be null, an empty string, or 'none'.
*/
if ( self::is_valid_key( $offer_opts, 'offer_name' ) && self::is_valid_key( $offer_opts, 'offer_price' ) ) {
if ( false !== ( $offer = self::get_data_itemprop_from_assoc( $offer_opts, array(
'name' => 'offer_name',
'price' => 'offer_price',
'priceCurrency' => 'offer_currency',
'availability' => 'offer_avail', // In stock, Out of stock, Pre-order, etc.
) ) ) ) {
$offer[ 'url' ] = $json_data[ 'url' ];
$offer[ 'priceValidUntil' ] = self::get_schema_product_price_valid_date();
$json_data[ 'offers' ][] = self::get_schema_type_context( 'https://schema.org/Offer', $offer );
}
}
}
}
/*
* Item Reviewed: Place
*/
} elseif ( $wpsso->schema->is_schema_type_child( $type_id, 'place' ) ) {
/*
* Property:
* address as https://schema.org/PostalAddress
*/
if ( ! empty( $md_opts[ 'schema_review_item_place_street_address' ] ) ) {
$json_data[ 'address' ] = self::get_schema_type_context( 'https://schema.org/PostalAddress',
self::get_data_itemprop_from_assoc( $md_opts, array(
'streetAddress' => 'schema_review_item_place_street_address',
'postOfficeBoxNumber' => 'schema_review_item_place_po_box_number',
'addressLocality' => 'schema_review_item_place_city',
'addressRegion' => 'schema_review_item_place_region',
'postalCode' => 'schema_review_item_place_postal_code',
'addressCountry' => 'schema_review_item_place_country', // Alpha2 country code.
) )
);
}
if ( $wpsso->schema->is_schema_type_child( $type_id, 'local.business' ) ) {
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'priceRange' => 'schema_review_item_place_price_range',
) );
if ( $wpsso->schema->is_schema_type_child( $type_id, 'food.establishment' ) ) {
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'servesCuisine' => 'schema_review_item_place_cuisine',
) );
}
}
/*
* Item Reviewed: Product
*/
} elseif ( $wpsso->schema->is_schema_type_child( $type_id, 'product' ) ) {
self::add_data_itemprop_from_assoc( $json_data, $md_opts, array(
'sku' => 'schema_review_item_product_retailer_part_no',
'mpn' => 'schema_review_item_product_mfr_part_no',
) );
/*
* Add the product brand.
*/
$single_brand = self::get_data_itemprop_from_assoc( $md_opts, array(
'name' => 'schema_review_item_product_brand',
) );
if ( false !== $single_brand ) { // Just in case.
$json_data[ 'brand' ] = self::get_schema_type_context( 'https://schema.org/Brand', $single_brand );
}
$md_offers_max = SucomUtil::get_const( 'WPSSO_SCHEMA_METADATA_OFFERS_MAX' );
foreach ( range( 0, $md_offers_max - 1, 1 ) as $key_num ) {
$offer_opts = SucomUtil::preg_grep_keys( '/^schema_review_item_product_(offer_.*)_' . $key_num. '$/',
$md_opts, $invert = false, $replace = '$1' );
/*
* Must have at least an offer name and price.
*
* Values cannot be null, an empty string, or 'none'.
*/
if ( self::is_valid_key( $offer_opts, 'offer_name' ) && self::is_valid_key( $offer_opts, 'offer_price' ) ) {
if ( false !== ( $offer = self::get_data_itemprop_from_assoc( $offer_opts, array(
'name' => 'offer_name',
'price' => 'offer_price',
'priceCurrency' => 'offer_currency',
'availability' => 'offer_avail', // In stock, Out of stock, Pre-order, etc.
) ) ) ) {
/*
* Add the offer.
*/
$json_data[ 'offers' ][] = self::get_schema_type_context( 'https://schema.org/Offer', $offer );
}
}
}
}
}
/*
* See WpssoJsonTypeItemList->filter_json_data_https_schema_org_itemlist().
*/
public static function add_itemlist_data( &$json_data, array $mod, array $mt_og, $page_type_id, $is_main ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$prop_name = 'itemListElement';
$item_count = isset( $json_data[ $prop_name ] ) ? count( $json_data[ $prop_name ] ) : 0;
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $prop_name . ' property provided ' . $item_count . ' elements' );
}
if ( empty( $page_type_id ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: page type id is empty and required' );
}
return $item_count;
}
/*
* Create markup specifically for the Schema ItemList type (not its sub-types like breadcrumb.list,
* howto.section, howto.step, or offer.catalog).
*/
if ( 'item.list' === $page_type_id ) {
$json_data[ 'itemListOrder' ] = 'https://schema.org/ItemListUnordered';
if ( isset( $mod[ 'query_vars' ][ 'order' ] ) ) {
switch ( $mod[ 'query_vars' ][ 'order' ] ) {
case 'ASC':
$json_data[ 'itemListOrder' ] = 'https://schema.org/ItemListOrderAscending';
break;
case 'DESC':
$json_data[ 'itemListOrder' ] = 'https://schema.org/ItemListOrderDescending';
break;
}
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'calling page_posts_mods()' );
}
$page_posts_mods = $wpsso->page->get_posts_mods( $mod );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'page_posts_mods array has ' . count( $page_posts_mods ) . ' elements' );
}
if ( empty( $json_data[ $prop_name ] ) ) {
$json_data[ $prop_name ] = array();
} elseif ( ! is_array( $json_data[ $prop_name ] ) ) { // Convert single value to an array.
$json_data[ $prop_name ] = array( $json_data[ $prop_name ] );
}
foreach ( $page_posts_mods as $post_mod ) {
$item_count++;
$post_canonical_url = $wpsso->util->get_canonical_url( $post_mod );
$post_json_data = self::get_schema_type_context( 'https://schema.org/ListItem', array(
'position' => $item_count,
'url' => $post_canonical_url,
) );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding post ID ' . $post_mod[ 'id' ] . ' to ' . $prop_name . ' as #' . $item_count );
}
$json_data[ $prop_name ][] = $post_json_data; // Add the post data.
}
$filter_name = SucomUtil::sanitize_hookname( 'wpsso_json_prop_https_schema_org_' . $prop_name );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
}
$json_data[ $prop_name ] = apply_filters( $filter_name, $json_data[ $prop_name ], $mod, $mt_og, $page_type_id, $is_main );
self::check_required_props( $json_data, $mod, array( $prop_name ), $page_type_id );
}
return $item_count;
}
/*
* $mt_og can be the main webpage open graph array or a product $mt_offer array.
*
* $size_names can be null, a string, or an array.
*
* $add_video can be true, false, or a string (property name).
*/
public static function add_media_data( &$json_data, $mod, $mt_og, $size_names = 'schema', $add_video = true ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$img_added = 0;
$max_nums = $wpsso->util->get_max_nums( $mod, 'og' );
$mt_images = $wpsso->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre = array( 'schema', 'og' ) );
if ( ! empty( $mt_images ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding images to json data' );
}
$img_added = self::add_images_data_mt( $json_data[ 'image' ], $mt_images );
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $img_added . ' images added' );
}
/*
* Allow the video property to be skipped - some schema types (organization, for example) do not include a video property.
*/
if ( $add_video ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding all video(s)' );
}
$vid_added = 0;
$vid_prop = is_string( $add_video ) ? $add_video : 'video';
if ( ! empty( $mt_og[ 'og:video' ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding videos to json data "' . $vid_prop . '" property' );
}
$vid_added = self::add_videos_data_mt( $json_data[ $vid_prop ], $mt_og[ 'og:video' ], 'og:video' );
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $vid_added . ' videos added to "' . $vid_prop . '" property' );
}
} elseif ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping videos: add_video argument is false' );
}
/*
* Redefine mainEntityOfPage property for Attachment pages.
*
* If this is an attachment page, and the post mime_type is a known media type (image, video, or audio),
* then set the first media array element mainEntityOfPage to the page url, and set the page
* mainEntityOfPage property to false (so it doesn't get defined later).
*/
$main_prop = $mod[ 'is_attachment' ] ? $mod[ 'post_mime_group' ] : '';
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'applying filters "wpsso_json_media_main_prop"' );
}
$main_prop = apply_filters( 'wpsso_json_media_main_prop', $main_prop, $mod );
if ( ! empty( $main_prop ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $mod[ 'name' ] . ' id ' . $mod[ 'id' ] . ' "' . $main_prop . '" property is main entity' );
}
if ( empty( $json_data[ $main_prop ] ) || ! is_array( $json_data[ $main_prop ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $mod[ 'name' ] . ' id ' . $mod[ 'id' ] . ' "' . $main_prop . '" property is empty or not an array' );
}
} else {
reset( $json_data[ $main_prop ] );
$media_key = key( $json_data[ $main_prop ] ); // Media array key should be '0'.
if ( ! isset( $json_data[ $main_prop ][ $media_key ][ 'mainEntityOfPage' ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'mainEntityOfPage for "' . $main_prop . '" key ' . $media_key . ' = ' . $mt_og[ 'og:url' ] );
}
$json_data[ $main_prop ][ $media_key ][ 'mainEntityOfPage' ] = $mt_og[ 'og:url' ];
} elseif ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'mainEntityOfPage for "' . $main_prop . '" key ' . $media_key . ' already defined' );
}
$json_data[ 'mainEntityOfPage' ] = false;
}
}
}
public static function add_offer_catalogs_data( &$json_data, $mod, $md_opts, $opt_pre = 'schema_offer_catalog', $prop_name = 'hasOfferCatalog' ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
/*
* Get the offer catalog names.
*/
$catalogs = SucomUtil::get_multi_values( $md_opts, $opt_pre );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log_arr( 'catalogs', $catalogs );
}
if ( ! empty( $catalogs ) ) { // Just in case.
$catalog_data = array();
$catalog_pos = 0;
/*
* $md_val is the offer catalog name.
*/
foreach ( $catalogs as $num => $md_val ) {
/*
* Maybe get a longer text / description value.
*/
$catalog_text = isset( $md_opts[ $opt_pre . '_text_' . $num ] ) ? $md_opts[ $opt_pre . '_text_' . $num ] : null;
if ( empty( $md_val ) && empty( $catalog_text ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping catalog ' . $num . ': catalog name and text are empty' );
}
continue;
}
$catalog_pos++;
$catalog_url = isset( $md_opts[ $opt_pre . '_url_' . $num ] ) ? $md_opts[ $opt_pre . '_url_' . $num ] : null;
$catalog_data[] = self::get_schema_type_context( 'https://schema.org/ListItem',
array(
'position' => $catalog_pos,
'url' => $catalog_url,
'name' => $md_val, // Step name.
'text' => $catalog_text,
)
);
}
$json_data[ $prop_name ] = self::get_schema_type_context( 'https://schema.org/OfferCatalog',
array(
'numberOfItems' => $catalog_pos,
'itemListOrder' => 'https://schema.org/ItemListUnordered',
'itemListElement' => $catalog_data,
)
);
}
}
public static function add_offers_aggregate_data_mt( &$json_data, array $mt_offers ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$aggr_added = 0;
$aggr_prices = array();
$aggr_offers = array();
$aggr_common = array();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding ' . count( $mt_offers ) . ' offers as AggregateOffer' );
}
foreach ( $mt_offers as $num => $mt_offer ) {
if ( ! is_array( $mt_offer ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping offer #' . $num . ': not an array' );
}
continue;
}
if ( ! $mod = $wpsso->og->get_product_retailer_item_mod( $mt_offer ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping offer #' . $num . ' no post id for retailer item id' );
}
continue;
}
$single_offer = WpssoSchemaSingle::get_offer_data_mt( $mod, $mt_offer, $def_type_id = 'offer' );
if ( empty( $single_offer[ 'priceCurrency' ] ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping offer #' . $num . ': missing price currency' );
}
continue;
}
/*
* Keep track of the lowest and highest price by currency.
*/
$price_currency = $single_offer[ 'priceCurrency' ]; // Shortcut variable.
if ( isset( $single_offer[ 'price' ] ) ) { // Just in case.
if ( ! isset( $aggr_prices[ $price_currency ][ 'lowPrice' ] ) ||
$aggr_prices[ $price_currency ][ 'lowPrice' ] > $single_offer[ 'price' ] ) {
$aggr_prices[ $price_currency ][ 'lowPrice' ] = $single_offer[ 'price' ];
}
if ( ! isset( $aggr_prices[ $price_currency ][ 'highPrice' ] ) ||
$aggr_prices[ $price_currency ][ 'highPrice' ] < $single_offer[ 'price' ] ) {
$aggr_prices[ $price_currency ][ 'highPrice' ] = $single_offer[ 'price' ];
}
}
/*
* Save common properties (by currency) to include in the AggregateOffer markup.
*/
if ( 0 === $num ) {
foreach ( preg_grep( '/^[^@]/', array_keys( $single_offer ) ) as $key ) {
$aggr_common[ $price_currency ][ $key ] = $single_offer[ $key ];
}
} elseif ( ! empty( $aggr_common[ $price_currency ] ) ) {
foreach ( $aggr_common[ $price_currency ] as $key => $val ) {
if ( ! isset( $single_offer[ $key ] ) ) {
unset( $aggr_common[ $price_currency ][ $key ] );
} elseif ( $val !== $single_offer[ $key ] ) {
unset( $aggr_common[ $price_currency ][ $key ] );
}
}
}
/*
* Add the complete offer.
*/
$aggr_offers[ $price_currency ][] = $single_offer;
}
/*
* Add aggregate offers grouped by currency.
*/
foreach ( $aggr_offers as $price_currency => $currency_offers ) {
if ( ( $offer_count = count( $currency_offers ) ) > 0 ) {
$offer_group = array();
/*
* Maybe set the 'lowPrice' and 'highPrice' properties.
*/
foreach ( array( 'lowPrice', 'highPrice' ) as $price_mark ) {
if ( isset( $aggr_prices[ $price_currency ][ $price_mark ] ) ) {
$offer_group[ $price_mark ] = $aggr_prices[ $price_currency ][ $price_mark ];
}
}
$offer_group[ 'priceCurrency' ] = $price_currency;
if ( ! empty( $aggr_common[ $price_currency ] ) ) {
foreach ( $aggr_common[ $price_currency ] as $key => $val ) {
$offer_group[ $key ] = $val;
}
}
$offer_group[ 'offerCount' ] = $offer_count;
$offer_group[ 'offers' ] = $currency_offers;
$json_data[ 'offers' ][] = self::get_schema_type_context( 'https://schema.org/AggregateOffer', $offer_group );
$aggr_added++;
}
}
return $aggr_added;
}
public static function add_offers_data_mt( &$json_data, array $mt_offers ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$offers_added = 0;
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding ' . count( $mt_offers ) . ' offers as Offer' );
}
foreach ( $mt_offers as $num => $mt_offer ) {
if ( ! is_array( $mt_offer ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping offer #' . $num . ': not an array' );
}
continue;
}
if ( ! $mod = $wpsso->og->get_product_retailer_item_mod( $mt_offer ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping offer #' . $num . ' no post id for retailer item id' );
}
continue;
}
$single_offer = WpssoSchemaSingle::get_offer_data_mt( $mod, $mt_offer, $def_type_id = 'offer' );
if ( false === $single_offer ) {
continue;
}
$json_data[ 'offers' ][] = $single_offer;
$offers_added++;
}
return $offers_added;
}
/*
* Called by the Blog, CollectionPage, ProfilePage, and SearchResultsPage filters.
*
* Example:
*
* $prop_type_ids = array( 'mentions' => false )
* $prop_type_ids = array( 'blogPosting' => 'blog.posting' )
*
* Do not cast $prop_type_ids as an array to allow for backwards compatibility.
*/
public static function add_posts_data( &$json_data, array $mod, array $mt_og, $page_type_id, $is_main, $prop_type_ids ) {
static $added_page_type_ids = array();
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$added_count = 0; // Initialize the total posts added counter.
if ( ! is_array( $prop_type_ids ) && is_array( $deprecated ) ) {
$prop_type_ids = $deprecated;
$deprecated = null;
}
/*
* Sanity checks.
*/
if ( empty( $page_type_id ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: page type id is empty' );
}
return $added_count;
} elseif ( empty( $prop_type_ids ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: prop type ids is empty' );
}
return $added_count;
}
/*
* Prevent recursion - i.e. webpage.collection in webpage.collection, etc.
*/
if ( isset( $added_page_type_ids[ $page_type_id ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: preventing recursion of page type id ' . $page_type_id );
}
return $added_count;
} else $added_page_type_ids[ $page_type_id ] = true;
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark( 'adding posts data' ); // Begin timer.
}
$page_posts_mods = $wpsso->page->get_posts_mods( $mod );
if ( empty( $page_posts_mods ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: page posts mods array is empty' );
$wpsso->debug->mark( 'adding posts data' ); // End timer.
}
return $added_count;
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'page_posts_mods array has ' . count( $page_posts_mods ) . ' elements' );
}
/*
* Set the Schema properties.
*/
foreach ( $prop_type_ids as $prop_name => $type_ids ) {
if ( empty( $type_ids ) ) { // False or empty array - allow any schema type.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'any schema type is allowed for prop_name ' . $prop_name );
}
$type_ids = array( 'any' );
} elseif ( is_string( $type_ids ) ) { // Convert value to an array.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'only schema type ' . $type_ids . ' allowed for prop_name ' . $prop_name );
}
$type_ids = array( $type_ids );
} elseif ( ! is_array( $type_ids ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping prop_name ' . $prop_name . ': value must be false, string, or array of schema types' );
}
continue;
}
if ( empty( $json_data[ $prop_name ] ) ) {
$json_data[ $prop_name ] = array();
} elseif ( ! is_array( $json_data[ $prop_name ] ) ) { // Convert single value to an array.
$json_data[ $prop_name ] = array( $json_data[ $prop_name ] );
}
$prop_count = count( $json_data[ $prop_name ] ); // Initialize the posts per property name counter.
foreach ( $page_posts_mods as $post_mod ) {
$post_type_id = $wpsso->schema->get_mod_schema_type_id( $post_mod );
$add_post_data = false;
foreach ( $type_ids as $family_member_id ) {
if ( 'any' === $family_member_id ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'accepting post ID ' . $post_mod[ 'id' ] . ': any schema type is allowed' );
}
$add_post_data = true;
break; // One positive match is enough.
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking if schema type ' . $post_type_id . ' is child of ' . $family_member_id );
}
$mod_is_child = $wpsso->schema->is_schema_type_child( $post_type_id, $family_member_id );
if ( $mod_is_child ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'accepting post ID ' . $post_mod[ 'id' ] . ': ' .
$post_type_id . ' is child of ' . $family_member_id );
}
$add_post_data = true;
break; // One positive match is enough.
} elseif ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'post ID ' . $post_mod[ 'id' ] . ' schema type ' .
$post_type_id . ' not a child of ' . $family_member_id );
}
}
if ( ! $add_post_data ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping post ID ' . $post_mod[ 'id' ] . ' for prop_name ' . $prop_name );
}
continue;
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'getting single mod data for post ID ' . $post_mod[ 'id' ] );
}
$post_json_data = $wpsso->schema->get_mod_json_data( $post_mod );
if ( empty( $post_json_data ) ) { // Prevent null assignment.
$wpsso->debug->log( 'single mod data for post ID ' . $post_mod[ 'id' ] . ' is empty' );
continue; // Get the next post mod.
}
$added_count++;
$prop_count++;
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding post ID ' . $post_mod[ 'id' ] . ' to ' . $prop_name . ' as #' . $prop_count );
}
$json_data[ $prop_name ][] = $post_json_data; // Add the post data.
}
$filter_name = SucomUtil::sanitize_hookname( 'wpsso_json_prop_https_schema_org_' . $prop_name );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
}
$json_data[ $prop_name ] = apply_filters( $filter_name, $json_data[ $prop_name ], $mod, $mt_og, $page_type_id, $is_main );
}
unset( $added_page_type_ids[ $page_type_id ] );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark( 'adding posts data' ); // End timer.
}
return $added_count;
}
public static function add_person_names_data( &$json_data, $prop_name, array $assoc, $assoc_key = '' ) {
if ( ! empty( $prop_name ) && ! empty( $assoc_key ) ) {
foreach ( SucomUtil::get_multi_values( $assoc, $assoc_key ) as $value ) {
$json_data[ $prop_name ][] = self::get_schema_type_context( 'https://schema.org/Person', array( 'name' => $value ) );
}
}
}
/*
* See WpssoSchemaSingle->add_product_group_data_mt().
*/
public static function add_variants_data_mt( &$json_data, array $mt_variants ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$variants_added = 0;
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding ' . count( $mt_variants ) . ' variants as Product' );
}
foreach ( $mt_variants as $num => $mt_variant ) {
if ( ! is_array( $mt_variant ) ) { // Just in case.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping variant #' . $num . ': not an array' );
}
continue;
}
if ( ! $mod = $wpsso->og->get_product_retailer_item_mod( $mt_variant ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping variant #' . $num . ' no post id for retailer item id' );
}
continue;
}
$single_variant = WpssoSchemaSingle::get_product_data_mt( $mod, $mt_variant, $def_type_id = 'product' );
if ( false === $single_variant ) {
continue;
}
$json_data[ 'hasVariant' ][] = $single_variant;
$variants_added++;
}
return $variants_added;
}
/*
* Provide a single or two-dimension video array in $mt_videos.
*/
public static function add_videos_data_mt( &$json_data, $mt_videos, $media_pre = 'og:video' ) {
$videos_added = 0;
if ( isset( $mt_videos[ 0 ] ) && is_array( $mt_videos[ 0 ] ) ) { // 2 dimensional array.
foreach ( $mt_videos as $mt_single_video ) {
$videos_added += WpssoSchemaSingle::add_video_data_mt( $json_data, $mt_single_video, $media_pre, $list_el = true );
}
} elseif ( is_array( $mt_videos ) ) {
$videos_added += WpssoSchemaSingle::add_video_data_mt( $json_data, $mt_videos, $media_pre, $list_el = true );
}
return $videos_added; // return count of videos added
}
/*
* Return any third-party and custom post options for a given type and type ID.
*
* See wpsso_get_post_event_options() in lib/functions.php
* See wpsso_get_post_job_options() in lib/functions.php
*/
public static function get_post_type_options( $post_id, $type, $type_id = false ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( empty( $post_id ) ) { // Just in case.
return false;
} elseif ( empty( $type ) ) { // Just in case.
return false;
} elseif ( ! empty( $wpsso->post ) ) { // Just in case.
$mod = $wpsso->post->get_mod( $post_id );
} else return false;
$filter_name = SucomUtil::sanitize_hookname( 'wpsso_get_' . $type . '_options' );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'applying filters "' . $filter_name . '"' );
}
$type_opts = apply_filters( $filter_name, false, $mod, $type_id );
if ( ! empty( $type_opts ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log_arr( 'get_' . $type . '_options filters returned', $type_opts );
}
}
/*
* Add metadata defaults and custom values to the $type_opts array.
*
* $type_opts can be false, an empty array, or an array of one or more options.
*/
self::add_type_opts_md_pad( $type_opts, $mod, array( $type => 'schema_' . $type ) );
return $type_opts;
}
/*
* Add metadata defaults and custom values to the $type_opts array.
*
* $type_opts can be false, an empty array, or an array of one or more options.
*/
public static function add_type_opts_md_pad( &$type_opts, array $mod, array $opts_md_pre = array() ) {
if ( ! empty( $mod[ 'obj' ] ) ) { // Just in case.
$md_defs = $mod[ 'obj' ]->get_defaults( $mod[ 'id' ] );
$md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ] );
if ( empty( $opts_md_pre ) ) { // Nothing to rename.
$type_opts = array_merge( $md_defs, $md_opts );
} else foreach ( $opts_md_pre as $opt_key => $md_pre ) {
$md_defs = SucomUtil::preg_grep_keys( '/^' . $md_pre . '_/', $md_defs, $invert = false, $opt_key . '_' );
$md_opts = SucomUtil::preg_grep_keys( '/^' . $md_pre . '_/', $md_opts, $invert = false, $opt_key . '_' );
if ( is_array( $type_opts ) ) {
$type_opts = array_merge( $md_defs, $type_opts, $md_opts );
} else $type_opts = array_merge( $md_defs, $md_opts );
}
}
}
/*
* Since WPSSO Core v22.3.0.
*/
public static function add_type_data_areaserved( &$json_data, array $mod, $type_opts, $type_id, $opt_pre ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( ! empty( $type_opts[ $opt_pre . '_latitude' ] ) &&
! empty( $type_opts[ $opt_pre . '_longitude' ] ) &&
! empty( $type_opts[ $opt_pre . '_radius' ] ) ) {
$json_data[ 'areaServed' ][] = WpssoSchema::get_schema_type_context( 'https://schema.org/GeoCircle', array(
'geoMidpoint' => WpssoSchema::get_schema_type_context( 'https://schema.org/GeoCoordinates', array(
'latitude' => $type_opts[ $opt_pre . '_latitude' ],
'longitude' => $type_opts[ $opt_pre . '_longitude' ],
) ),
'geoRadius' => $type_opts[ $opt_pre . '_radius' ],
) );
}
foreach ( SucomUtil::get_multi_values( $type_opts, $opt_pre . '_area_id' ) as $id ) { // Uses is_valid_option_value().
WpssoSchemaSingle::add_place_data( $json_data[ 'areaServed' ], $mod, $id, $list_el = true );
}
}
/*
* Since WPSSO Core v22.2.0.
*
* See WpssoSchemaSingle::add_book_data().
* See WpssoSchemaSingle::add_event_data().
* See WpssoSchemaSingle::add_service_data().
*/
public static function add_type_data_offers( &$json_data, array $mod, $type_opts, $type_id, $opt_pre ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
self::add_type_opts_offers( $type_opts, $mod, $type_id, $opt_pre );
if ( ! empty( $type_opts[ $opt_pre . '_offers' ] ) && is_array( $type_opts[ $opt_pre . '_offers' ] ) ) {
foreach ( $type_opts[ $opt_pre . '_offers' ] as $offer ) {
if ( ! is_array( $offer ) ) { // Just in case.
continue;
}
if ( false !== ( $offer = self::get_data_itemprop_from_assoc( $offer, array(
'name' => 'offer_name',
'url' => 'offer_url',
'price' => 'offer_price',
'priceCurrency' => 'offer_currency',
'availability' => 'offer_avail', // In stock, Out of stock, Pre-order, etc.
'validFrom' => 'offer_start_date',
'validThrough' => 'offer_end_date',
) ) ) ) {
/*
* Add the offer.
*/
$json_data[ 'offers' ][] = self::get_schema_type_context( 'https://schema.org/Offer', $offer );
}
}
}
}
/*
* Since WPSSO Core v22.3.0.
*/
public static function add_type_data_sameas( &$json_data, array $mod, $type_opts, $type_id, $opt_pre ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( ! empty( $type_opts[ $opt_pre . '_sameas' ] ) && is_array( $type_opts[ $opt_pre . '_sameas' ] ) ) { // Just in case.
foreach ( $type_opts[ $opt_pre . '_sameas' ] as $url ) {
$json_data[ 'sameAs' ][] = SucomUtil::esc_url_encode( $url );
}
/*
* Sanitize the sameAs array - make sure URLs are valid and remove any duplicates.
*/
self::check_prop_value_sameas( $json_data );
}
}
/*
* Since WPSSO Core v22.2.0.
*/
public static function add_type_opts_offers( &$type_opts, array $mod, $type_id, $type ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking for custom ' . $type . ' offers' );
}
$type = SucomUtil::sanitize_key( $type ); // Just in case.
$md_offers_max = SucomUtil::get_const( 'WPSSO_SCHEMA_METADATA_OFFERS_MAX' );
$canonical_url = $wpsso->util->get_canonical_url( $mod );
$offers_count = 0;
foreach ( range( 0, $md_offers_max - 1, 1 ) as $key_num ) {
$offer_opts = apply_filters( 'wpsso_get_' . $type . '_offer_options', false, $mod, $type_id, $key_num );
if ( ! empty( $offer_opts ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log_arr( 'get_' . $type . '_offer_options #'. $key_num, $offer_opts );
}
}
if ( ! is_array( $offer_opts ) ) {
$offer_opts = array();
foreach ( array(
'offer_name' => 'schema_' . $type . '_offer_name',
'offer_url' => 'schema_' . $type . '_offer_url',
'offer_price' => 'schema_' . $type . '_offer_price',
'offer_currency' => 'schema_' . $type . '_offer_currency',
'offer_avail' => 'schema_' . $type . '_offer_avail',
) as $opt_key => $md_pre ) {
$offer_opts[ $opt_key ] = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_pre . '_' . $key_num );
}
}
/*
* Must have at least an offer name and price.
*/
if ( isset( $offer_opts[ 'offer_name' ] ) && isset( $offer_opts[ 'offer_price' ] ) ) {
if ( ! isset( $type_opts[ 'offer_url' ] ) ) {
$offer_opts[ 'offer_url' ] = $canonical_url;
}
if ( ! isset( $offer_opts[ 'offer_start_date' ] ) ) {
if ( ! empty( $type_opts[ $type . '_offers_start_date_iso' ] ) ) {
$offer_opts[ 'offer_start_date' ] = $type_opts[ $type . '_offers_start_date_iso' ];
}
}
if ( ! isset( $offer_opts[ 'offer_end_date' ] ) ) {
if ( ! empty( $type_opts[ $type . '_offers_end_date_iso' ] ) ) {
$offer_opts[ 'offer_end_date' ] = $type_opts[ $type . '_offers_end_date_iso' ];
}
}
if ( 0 === $offers_count ) {
$type_opts[ $type . '_offers' ] = array(); // Clear offers returned by filter.
}
$offers_count++;
$type_opts[ $type . '_offers' ][] = $offer_opts;
}
}
return $offers_count;
}
/*
* Get dates from the meta data options and add ISO formatted dates to the array (passed by reference).
*/
public static function add_mod_opts_date_iso( array $mod, &$opts, array $opts_md_pre ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
foreach ( $opts_md_pre as $opt_pre => $md_pre ) {
$date_iso = self::get_mod_date_iso( $mod, $md_pre );
if ( ! is_array( $opts ) ) { // Just in case.
$opts = array();
}
$opts[ $opt_pre . '_iso' ] = $date_iso;
}
}
/*
* See WpssoMedia->get_all_videos().
*/
public static function get_mod_date_iso( array $mod, $md_pre, $def_date = null ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( ! is_string( $md_pre ) ) { // Just in case.
return '';
}
$md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ] );
if ( ! empty( $def_date ) ) {
$def_opts = array();
self::add_date_time_timezone_opts( $def_date, $def_opts, $md_pre );
$md_opts = array_merge( $def_opts, $md_opts );
}
return self::get_opts_date_iso( $md_opts, $md_pre );
}
public static function get_opts_date_iso( array $opts, $md_pre ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
if ( ! is_string( $md_pre ) ) { // Just in case.
return '';
}
$md_date = empty( $opts[ $md_pre . '_date' ] ) || 'none' === $opts[ $md_pre . '_date' ] ? '' : $opts[ $md_pre . '_date' ];
$md_time = empty( $opts[ $md_pre . '_time' ] ) || 'none' === $opts[ $md_pre . '_time' ] ? '' : $opts[ $md_pre . '_time' ];
$md_timezone = empty( $opts[ $md_pre . '_timezone' ] ) || 'none' === $opts[ $md_pre . '_timezone' ] ? '' : $opts[ $md_pre . '_timezone' ];
if ( empty( $md_date ) ) { // No date or time.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: ' . $md_pre . ' date is empty' );
}
return ''; // Nothing to do.
}
if ( empty( $md_time ) ) { // Date with no time.
$md_time = '00:00';
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $md_pre . ' time is empty: using time ' . $md_time );
}
}
if ( empty( $md_timezone ) ) { // No timezone.
$md_timezone = $wpsso->get_options( 'og_def_timezone', 'UTC' );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $md_pre . ' timezone is empty: using timezone ' . $md_timezone );
}
}
$date_obj = date_create( $md_date . ' ' . $md_time . ' ' . $md_timezone );
return date_format( $date_obj, 'c' );
}
public static function add_date_time_timezone_opts( $date, array &$opts, $md_pre ) { // Pass by reference is OK.
$opts[ $md_pre . '_date' ] = '';
$opts[ $md_pre . '_time' ] = '';
$opts[ $md_pre . '_timezone' ] = '';
if ( $date ) {
$date_obj = date_create( $date );
if ( $date_obj ) {
/*
* See https://www.php.net/manual/en/datetime.format.php.
*/
$opts[ $md_pre . '_date' ] = date_format( $date_obj, 'Y-m-d' );
$opts[ $md_pre . '_time' ] = date_format( $date_obj, 'H:i' );
$opts[ $md_pre . '_timezone' ] = date_format( $date_obj, 'P' );
}
}
}
/*
* Example $prop_names array:
*
* array(
* 'prepTime' => 'schema_recipe_prep',
* 'cookTime' => 'schema_recipe_cook',
* 'totalTime' => 'schema_recipe_total',
* );
*/
public static function add_data_time_from_assoc( array &$json_data, array $assoc, array $prop_names ) {
foreach ( $prop_names as $prop_name => $assoc_key ) {
$t = array();
foreach ( array( 'days', 'hours', 'mins', 'secs' ) as $time_incr ) {
$t[ $time_incr ] = empty( $assoc[ $assoc_key . '_' . $time_incr ] ) ? // 0 or empty string.
0 : (int) $assoc[ $assoc_key . '_' . $time_incr ]; // Define as 0 by default.
}
if ( $t[ 'days' ] . $t[ 'hours' ] . $t[ 'mins' ] . $t[ 'secs' ] > 0 ) {
$json_data[ $prop_name ] = 'P' . $t[ 'days' ] . 'DT' . $t[ 'hours' ] . 'H' . $t[ 'mins' ] . 'M' . $t[ 'secs' ] . 'S';
}
}
}
/*
* Returns a https://schema.org/unitText value ('cm', 'ml', or 'kg').
*/
public static function get_unit_text( $mixed_key ) {
static $local_cache = array();
if ( isset( $local_cache[ $mixed_key ] ) ) {
return $local_cache[ $mixed_key ];
}
$match_key = null;
$schema_units = WpssoConfig::get_schema_units(); // Uses a local cache.
if ( is_array( $schema_units ) ) { // Just in case.
if ( isset( $schema_units[ $mixed_key ] ) ) {
$match_key = $mixed_key;
} else {
$mixed_key = str_replace( ':', '_', $mixed_key ); // Fix for meta tag names.
$unit_keys = array_keys( $schema_units );
foreach ( $unit_keys as $unit_key ) {
if ( false !== strpos( $mixed_key, '_' . $unit_key . '_value' ) ||
false !== strpos( $mixed_key, '_' . $unit_key . '_units' ) ) {
$match_key = $unit_key;
break;
}
}
}
}
if ( null !== $match_key ) {
if ( is_array( $schema_units[ $match_key ] ) ) { // Just in case.
foreach ( $schema_units[ $match_key ] as $prop_name => $prop_data ) {
if ( isset( $prop_data[ 'unitText' ] ) ) { // Return the first match.
return $local_cache[ $mixed_key ] = $prop_data[ 'unitText' ];
}
}
}
}
return $local_cache[ $mixed_key ] = '';
}
/*
* Examples $key_map array:
*
* $key_map = array(
* 'length' => 'product:length:value',
* 'width' => 'product:width:value',
* 'height' => 'product:height:value',
* 'weight' => 'product:weight:value',
* 'fluid_volume' => 'product:fluid_volume:value',
* );
*
* $key_map = array(
* 'width_px' => 'og:image:width',
* 'height_px' => 'og:image:height',
* );
*/
public static function add_data_unit_from_assoc( array &$json_data, array $assoc, array $key_map ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$schema_units = WpssoConfig::get_schema_units(); // Uses a local cache.
foreach ( $key_map as $unit_key => $assoc_key ) {
/*
* Make sure the property name we need (width, height, weight, etc.) is configured.
*/
if ( empty( $schema_units[ $unit_key ] ) || ! is_array( $schema_units[ $unit_key ] ) ) {
continue;
}
/*
* Exclude empty string values.
*/
if ( isset( $assoc[ $assoc_key ] ) ) {
$assoc[ $assoc_key ] = trim( $assoc[ $assoc_key ] ); // Just in case.
}
if ( ! isset( $assoc[ $assoc_key ] ) || '' === $assoc[ $assoc_key ] ) {
continue;
}
/*
* Example array:
*
* $schema_units[ 'depth' ] = array(
* 'depth' => array(
* '@context' => 'https://schema.org',
* '@type' => 'QuantitativeValue',
* 'name' => 'Depth',
* 'unitText' => 'cm',
* 'unitCode' => 'CMT',
* ),
* ),
*/
foreach ( $schema_units[ $unit_key ] as $prop_name => $prop_data ) {
$quant_id = 'qv-' . $unit_key . '-' . $assoc[ $assoc_key ]; // Example '@id' = '#sso/qv-width-px-1200'.
self::update_data_id( $prop_data, $quant_id, '/' );
$prop_data[ 'value' ] = $assoc[ $assoc_key ];
$json_data[ $prop_name ][] = $prop_data;
}
}
}
/*
* Returns the number of Schema properties added to $json_data.
*
* $json_data can be null, and will remain null unless an element is added, in which case it will become an array.
*
* Example usage:
*
* WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $mt_og, array(
* 'datePublished' => 'article:published_time',
* 'dateModified' => 'article:modified_time',
* ) );
*
* WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $org_opts, array(
* 'url' => 'org_url',
* 'name' => 'org_name',
* 'alternateName' => 'org_name_alt',
* 'description' => 'org_desc',
* 'email' => 'org_email',
* 'telephone' => 'org_phone',
* ) );
*
*/
public static function add_data_itemprop_from_assoc( &$json_data, array $assoc, array $key_map, $overwrite = true ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$is_assoc = SucomUtil::is_assoc( $key_map );
$prop_added = 0;
foreach ( $key_map as $prop_name => $assoc_key ) {
if ( ! $is_assoc ) $prop_name = $assoc_key;
if ( self::is_valid_key( $assoc, $assoc_key ) ) { // Not null, an empty string, or 'none'.
if ( ! is_array( $json_data ) ) $json_data = array();
if ( isset( $json_data[ $prop_name ] ) && empty( $overwrite ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping ' . $prop_name . ': itemprop exists and overwrite is false' );
}
continue;
}
if ( is_string( $assoc[ $assoc_key ] ) && false !== filter_var( $assoc[ $assoc_key ], FILTER_VALIDATE_URL ) ) {
$json_data[ $prop_name ] = SucomUtil::esc_url_encode( $assoc[ $assoc_key ] );
} else $json_data[ $prop_name ] = $assoc[ $assoc_key ];
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'assigned ' . $assoc_key . ' value to itemprop ' . $prop_name . ' = ' .
print_r( $json_data[ $prop_name ], true ) );
}
$prop_added++;
}
}
return $prop_added;
}
/*
* Checks both the array key and its value.
*
* The array key must exist, and its value cannot be null, an empty string, or 'none'.
*
* If the key is a width or height, the value cannot be -1.
*
* See WpssoJsonTypeMovie->filter_json_data_https_schema_org_movie().
* See WpssoSchema::add_item_reviewed_data().
* See WpssoSchema::add_data_itemprop_from_assoc().
* WpssoSchemaSingle::add_book_data().
* WpssoSchemaSingle::add_offer_data_mt().
* WpssoSchemaSingle::add_product_data_mt().
*/
public static function is_valid_key( $assoc, $key ) {
if ( null === $key ) { // Array key cannot be null.
return false;
} elseif ( ! isset( $assoc[ $key ] ) ) {
return false;
} elseif ( ! self::is_valid_val( $assoc[ $key ] ) ) { // Not null, empty string, or 'none'.
return false;
} elseif ( 'width' === $key || 'height' === $key ) {
if ( WPSSO_UNDEF === $assoc[ $key ] ) { // Invalid width or height.
return false;
}
}
return true;
}
/*
* The value cannot be null, an empty string, or the 'none' string.
*
* WpssoJsonTypeCreativeWork->filter_json_data_https_schema_org_creativework().
* See WpssoSchema::is_valid_key().
*/
public static function is_valid_val( $val ) {
if ( null === $val ) { // Null value is not valid.
return false;
} elseif ( '' === $val ) { // Empty string.
return false;
} elseif ( 'none' === $val ) { // Disabled option.
return false;
}
return true;
}
/*
* Since WPSSO Core v7.7.0.
*
* See WpssoJsonTypeQAPage->filter_json_data_https_schema_org_qapage().
*/
public static function move_data_itemprop_from_assoc( array &$json_data, array &$assoc, array $key_map, $overwrite = true ) {
$prop_added = self::add_data_itemprop_from_assoc( $json_data, $assoc, $key_map, $overwrite );
foreach ( $key_map as $prop_name => $assoc_key ) {
unset( $assoc[ $assoc_key ] );
}
return $prop_added;
}
/*
* Example usage:
*
* $offer = WpssoSchema::get_data_itemprop_from_assoc( $mt_offer, array(
* 'url' => 'product:url',
* 'name' => 'product:title',
* 'description' => 'product:description',
* 'mpn' => 'product:mfr_part_no',
* 'availability' => 'product:availability',
* 'itemCondition' => 'product:condition',
* 'price' => 'product:price:amount',
* 'priceCurrency' => 'product:price:currency',
* 'priceValidUntil' => 'product:sale_price_dates:end',
* ) );
*/
public static function get_data_itemprop_from_assoc( array $assoc, array $key_map, $exclude = array( '' ) ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
}
$json_data = array();
foreach ( $key_map as $prop_name => $assoc_key ) {
if ( isset( $assoc[ $assoc_key ] ) ) {
if ( ! in_array( $assoc[ $assoc_key ], $exclude, $strict = true ) ) {
$json_data[ $prop_name ] = $assoc[ $assoc_key ];
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'assigned ' . $assoc_key . ' value to itemprop ' .
$prop_name . ' = ' . print_r( $json_data[ $prop_name ], true ) );
}
}
}
}
return empty( $json_data ) ? false : $json_data;
}
public static function get_enumeration_examples( $enum_key, $val_prefix = '', $val_suffix = '' ) {
$wpsso =& Wpsso::get_instance();
$values = array();
if ( empty( $wpsso->cf[ 'form' ][ $enum_key ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $enum_key . ' enumeration key is unknown' );
}
} else {
$enumerations = $wpsso->cf[ 'form' ][ $enum_key ];
unset( $enumerations[ 'none' ] );
/*
* Include values without their comment / qualifier (for example, 'Adult (13 years old or more)').
*/
foreach ( $enumerations as $key => $val ) {
if ( false !== ( $pos = strpos( $val, '(' ) ) ) {
$enumerations[ $key ] = trim( substr( $val, 0, $pos ) );
}
}
$enums_transl = SucomUtilOptions::get_opts_values_transl( $enumerations, $text_domain = 'wpsso' );
foreach ( $enumerations as $key => $val ) {
$values[ $val ] = $val;
if ( false !== ( $pos = strpos( $key, 'https://schema.org/' ) ) ) {
$key = substr( $key, $pos + strlen( 'https://schema.org/' ) );
}
if ( $val_prefix && false !== ( $pos = strpos( $key, $val_prefix ) ) ) {
$key = substr( $key, $pos + strlen( $val_prefix ) );
}
if ( $val_suffix && false !== ( $pos = strpos( $key, $val_suffix ) ) ) {
$len_prefix = strlen( $key ) - strlen( $val_suffix );
if ( $pos === $len_prefix ) {
$key = substr( $key, 0, $len_prefix );
}
}
$values[ $key ] = $key;
}
SucomUtil::natasort( $values );
}
return $values;
}
/*
* Check for missing Schema property values.
*
* See WpssoJsonTypeCreativeWork->filter_json_data_https_schema_org_creativework().
* See WpssoSchema::add_itemlist_data().
* See WpssoSchemaSingle::add_product_data_mt().
*/
public static function check_required_props( &$json_data, array $mod, $prop_names = array( 'image' ), $type_id = null ) {
if ( ! $mod[ 'id' ] || ! $mod[ 'is_public' ] ) {
return;
} elseif ( $mod[ 'is_post' ] && 'publish' !== $mod[ 'post_status' ] ) {
return;
}
/*
* The post, term, or user has an ID, is public, and (in the case of a post) the post status is published.
*/
$wpsso =& Wpsso::get_instance();
$ref_url = $wpsso->util->maybe_set_ref( $canonical_url = null, $mod, __( 'checking schema properties', 'wpsso' ) );
list( $type_context, $type_name, $type_path ) = $wpsso->schema->get_schema_type_url_parts_by_id( $type_id );
foreach ( $prop_names as $prop_name ) {
if ( empty( $json_data[ $prop_name ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $prop_name . ' property value is empty and required' );
}
/*
* An is_admin() test is required to make sure the WpssoMessages class is available.
*/
if ( $wpsso->notice->is_admin_pre_notices() ) {
$notice_msg = $wpsso->msgs->get( 'notice-missing-schema-' . $prop_name, array( 'type_name' => $type_name ) );
$notice_key = $mod[ 'name' ] . '-' . $mod[ 'id' ] . '-notice-missing-schema-' . $prop_name;
if ( ! empty( $notice_msg ) ) { // Just in case.
$wpsso->notice->err( $notice_msg, null, $notice_key );
}
}
}
}
$wpsso->util->maybe_unset_ref( $ref_url );
}
/*
* Convert a numeric category ID to its Google category string.
*/
public static function check_prop_value_category( &$json_data ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking category property value' );
}
if ( ! empty( $json_data[ 'category' ] ) ) {
/*
* Category IDs are expected to be numeric Google category IDs.
*
* See https://www.google.com/basepages/producttype/taxonomy-with-ids.en-US.txt.
*/
if ( is_numeric( $json_data[ 'category' ] ) ) {
$cat_id = $json_data[ 'category' ];
$categories = $wpsso->util->get_google_product_categories();
if ( isset( $categories[ $cat_id ] ) ) {
$json_data[ 'category' ] = $categories[ $cat_id ];
} else unset( $json_data[ 'category' ] );
}
}
}
/*
* See WpssoSchema->filter_sanitize_md_options().
*/
public static function check_prop_value_enumeration( &$json_data, $prop_name, $enum_key, $val_prefix = '', $val_suffix = '' ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking ' . $prop_name . ' property value' );
}
if ( ! isset( $json_data[ $prop_name ] ) || ( empty( $json_data[ $prop_name ] ) && ! is_numeric( $json_data[ $prop_name ] ) ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $prop_name . ' property value is empty (and not numeric)' );
}
} elseif ( 'none' === $json_data[ $prop_name ] ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $prop_name . ' property value is none' );
}
} elseif ( empty( $wpsso->cf[ 'form' ][ $enum_key ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( $enum_key . ' enumeration key is unknown' );
}
} else {
$enumerations = $wpsso->cf[ 'form' ][ $enum_key ];
/*
* Include values without their comment / qualifier (for example, 'Adult (13 years old or more)').
*/
foreach ( $enumerations as $key => $val ) {
if ( false !== ( $pos = strpos( $val, '(' ) ) ) {
$enumerations[ $key ] = trim( substr( $val, 0, $pos ) );
}
}
$enums_labels = array_flip( $enumerations );
$enums_transl = array_flip( SucomUtilOptions::get_opts_values_transl( $enumerations, $text_domain = 'wpsso' ) );
$prop_val = $json_data[ $prop_name ]; // Example: 'New' or 'new'.
if ( ! isset( $enumerations[ $prop_val ] ) ) {
if ( isset( $enumerations[ $prop_val ] ) ) {
$json_data[ $prop_name ] = $prop_val;
} elseif ( isset( $enums_labels[ $prop_val ] ) ) {
$json_data[ $prop_name ] = $enums_labels[ $prop_val ];
} elseif ( isset( $enums_transl[ $prop_val ] ) ) {
$json_data[ $prop_name ] = $enums_transl[ $prop_val ];
} elseif ( isset( $enumerations[ 'https://schema.org/' . $prop_val ] ) ) {
$json_data[ $prop_name ] = 'https://schema.org/' . $prop_val;
} elseif ( isset( $enumerations[ 'https://schema.org/' . $val_prefix . $prop_val . $val_suffix ] ) ) {
$json_data[ $prop_name ] = 'https://schema.org/' . $val_prefix . $prop_val . $val_suffix;
} else {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'invalid ' . $prop_name . ' property value "' . $prop_val . '"' );
}
unset( $json_data[ $prop_name ] );
}
}
}
}
/*
* If we have a GTIN number, try to improve the assigned property name.
*
* Pass $json_data by reference to modify the array directly.
*
* A similar method exists as WpssoOpenGraph::check_mt_value_gtin().
*/
public static function check_prop_value_gtin( &$json_data ) {
$wpsso =& Wpsso::get_instance();
$gtin_props = array(
'gtin' => 0,
'gtin8' => 8,
'gtin12' => 12,
'gtin13' => 13,
'gtin14' => 14,
);
foreach ( $gtin_props as $prop_name => $prop_len ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking ' . $prop_name . ' property value' );
}
if ( isset( $json_data[ $prop_name ] ) ) {
/*
* The value may come from a custom field, so remove spaces and trim it, just in case.
*/
$json_data[ $prop_name ] = preg_replace( '/\s+/', '', $json_data[ $prop_name ] );
$gtin_len = strlen( $json_data[ $prop_name ] );
if ( $prop_len ) { // GTIN property has specific length.
if ( $gtin_len !== $prop_len ) { // GTIN property length mismatch.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'unsetting ' . $prop_name . ' property' );
}
unset( $json_data[ $prop_name ] );
}
}
}
}
if ( isset( $json_data[ 'gtin' ] ) ) {
$gtin_len = strlen( $json_data[ 'gtin' ] );
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'gtin property value length = ' . $gtin_len );
}
switch ( $gtin_len ) {
case 14:
case 13:
case 12:
case 8:
if ( empty( $json_data[ 'gtin' . $gtin_len ] ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'adding gtin' . $gtin_len . ' = ' . $json_data[ 'gtin' ] );
}
$json_data[ 'gtin' . $gtin_len ] = $json_data[ 'gtin' ];
}
break;
}
}
}
/*
* Sanitize the sameAs array - make sure URLs are valid and remove any duplicates.
*/
public static function check_prop_value_sameas( &$json_data ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'checking sameAs property value' );
}
if ( ! empty( $json_data[ 'sameAs' ] ) ) {
if ( ! is_array( $json_data[ 'sameAs' ] ) ) { // Just in case.
$json_data[ 'sameAs' ] = array( $json_data[ 'sameAs' ] );
}
$added_urls = array();
foreach ( $json_data[ 'sameAs' ] as $num => $url ) {
if ( empty( $url ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping sameAs url #' . $num . ': url is empty' );
}
} elseif ( isset( $json_data[ 'url' ] ) && $json_data[ 'url' ] === $url ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping sameAs url #' . $num . ': url "' . $url . '" is duplicate of url property' );
}
} elseif ( isset( $added_urls[ $url ] ) ) { // Already added.
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping sameAs url #' . $num . ': url "' . $url . '" is duplicate' );
}
} elseif ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'skipping sameAs url #' . $num . ': url "' . $url . '" is invalid' );
}
} else { // Mark the url as already added and get the next url.
$added_urls[ $url ] = true;
continue; // Get the next url.
}
unset( $json_data[ 'sameAs' ][ $num ] ); // Remove the duplicate / invalid url.
}
$json_data[ 'sameAs' ] = array_values( $json_data[ 'sameAs' ] ); // Reindex / renumber the array.
}
}
/*
* Returns false on error.
*
* $id_suffix can be a string, or an array.
*/
public static function update_data_id( &$json_data, $id_suffix, $data_url = null, $hash_url = false ) {
$wpsso =& Wpsso::get_instance();
if ( $wpsso->debug->enabled ) {
$wpsso->debug->mark();
$wpsso->debug->log_args( array(
'id_suffix' => $id_suffix,
'data_url' => $data_url,
'hash_url' => $hash_url,
) );
}
$id_anchor = self::get_id_anchor();
$id_delim = self::get_id_delim();
if ( is_array( $id_suffix ) ) {
$id_suffix = implode( $id_delim, $id_suffix );
}
$id_suffix = rtrim( $id_suffix, $id_delim ); // Just in case.
if ( empty( $id_suffix ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'exiting early: id suffix is empty and required' );
}
return false;
}
if ( $wpsso->debug->enabled ) {
if ( empty( $json_data[ '@id' ] ) ) {
$wpsso->debug->log( 'input @id property is empty' );
} else $wpsso->debug->log( 'input @id property is ' . $json_data[ '@id' ] );
}
/*
* If $id_suffix is a URL, then use it as-is.
*/
if ( false !== filter_var( $id_suffix, FILTER_VALIDATE_URL ) ) {
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'provided type_id is a valid url' );
}
unset( $json_data[ '@id' ] ); // Just in case.
$json_data = array( '@id' => $id_suffix ) + $json_data; // Make @id the first value in the array.
} else {
$id_url = '';
if ( ! empty( $data_url ) ) {
if ( is_string( $data_url ) ) { // Just in case.
$id_url = $data_url;
}
} elseif ( ! empty( $json_data[ '@id' ] ) ) {
$id_url = $json_data[ '@id' ];
} elseif ( ! empty( $json_data[ 'url' ] ) ) {
$id_url = $json_data[ 'url' ];
}
/*
* Maybe remove an anchor ID from the begining of the type id string.
*/
if ( 0 === strpos( $id_suffix, $id_anchor ) ) {
$id_suffix = substr( $id_suffix, strlen( $id_anchor ) - 1 );
}
/*
* Standardize the $id_suffix string.
*/
$id_suffix = preg_replace( '/[-_\. ]+/', '-', $id_suffix );
/*
* Check if we already have an anchor ID in the URL.
*/
if ( false === strpos( $id_url, $id_anchor ) ) {
/*
* Remove the URL fragment.
*
* Example: http://woo.surniaulula.com/product/a-variable-product/#comment-2.
*/
if ( false !== ( $pos = strpos( $id_url, '#' ) ) ) {
$id_url = substr( $id_url, 0, $pos );
}
$id_url .= $id_anchor;
}
/*
* Check if we already have the type id in the URL.
*/
if ( false === strpos( $id_url, $id_anchor . $id_suffix ) ) {
$id_url = trim( $id_url, $id_delim ) . $id_delim . $id_suffix;
}
unset( $json_data[ '@id' ] ); // Just in case.
$json_data = array( '@id' => $id_url ) + $json_data; // Make @id the first value in the array.
}
/*
* Possibly hash the '@id' URL to hide a WordPress login username (as one example). Since Google reads the
* '@id' value as a URL, use a leading slash to create the same path for the same '@id' URLs between
* different Schema JSON-LD scripts (ie. not relative to the current webpage). For example:
*
* "@id": "http://adm.surniaulula.com/author/manovotny/#sso/person"
* "@id": "/06d3730efc83058f497d3d44f2f364e3#sso/person"
*/
if ( $hash_url ) {
if ( preg_match( '/^(.*:\/\/.*)(' . preg_quote( $id_anchor, '/' ) . '.*)?$/U', $json_data[ '@id' ], $matches ) ) {
$md5_url = '/' . md5( $matches[ 1 ] ) . $matches[ 2 ];
$json_data[ '@id' ] = str_replace( $matches[ 0 ], $md5_url, $json_data[ '@id' ] );
}
}
if ( $wpsso->debug->enabled ) {
$wpsso->debug->log( 'returned @id property is ' . $json_data[ '@id' ] );
}
return true;
}
/*
* Sanitation used by filters to return their data.
*/
public static function return_data_from_filter( $json_data, $json_ret, $is_main = false ) {
if ( empty( $json_ret ) || ! is_array( $json_ret ) ) { // Just in case - nothing to merge.
return $json_data;
}
if ( $is_main ) {
foreach ( array( 'name', 'description', 'mainEntityOfPage' ) as $prop_name ) {
if ( ! empty( $json_data[ $prop_name ] ) ) {
unset( $json_ret[ $prop_name ] );
}
}
} else unset( $json_data[ 'mainEntityOfPage' ] );
if ( ! empty( $json_ret[ 'mainEntity' ] ) ) unset( $json_data[ 'mainEntity' ] );
$json_data = is_array( $json_data ) ? array_merge( $json_data, $json_ret ) : $json_ret;
unset( $json_ret );
if ( $is_main ) {
if ( ! isset( $json_data[ 'mainEntityOfPage' ] ) ) {
if ( ! empty( $json_data[ 'url' ] ) ) {
/*
* Remove any URL fragment from the main entity URL.
*
* The 'mainEntityOfPage' can be empty and will be removed by WpssoSchemaGraph::optimize_json().
*/
$json_data[ 'mainEntityOfPage' ] = preg_replace( '/#.*$/', '', $json_data[ 'url' ] );
}
}
}
return self::return_data_head_first( $json_data );
}
public static function return_data_head_first( $json_data ) {
if ( is_array( $json_data ) ) { // Just in case.
$json_head = array(
'@id' => null,
'@context' => null,
'@type' => null,
'mainEntityOfPage' => null,
);
$json_data = array_merge( $json_head, $json_data );
foreach ( $json_head as $prop_name => $prop_val ) {
if ( empty( $json_data[ $prop_name ] ) ) {
unset( $json_data[ $prop_name ] );
}
}
}
return $json_data;
}
/*
* See WpssoBcFilters->filter_json_data_https_schema_org_breadcrumblist().
* See WpssoSchemaGraph::optimize_json().
* See WpssoSchema::update_data_id().
*/
public static function get_id_anchor() {
return '#sso/';
}
public static function get_id_delim() {
return '/';
}
/*
* Add meta tags (like 'schema:review:rating') based on the filtered Schema markup.
*/
private function add_schema_mt_og( $single_graph, $mod, &$mt_og, $page_type_id, $is_main ) { // Pass by reference is OK.
if ( $is_main ) {
if ( 'review' === $page_type_id ) {
$mt_og[ 'schema:review:rating' ] = isset( $single_graph[ 'reviewRating' ][ 'ratingValue' ] ) ?
$single_graph[ 'reviewRating' ][ 'ratingValue' ] : null;
}
}
}
/*
* Add cross-references for schema sub-type arrays that exist under more than one type.
*
* For example, Thing > Place > LocalBusiness also exists under Thing > Organization > LocalBusiness.
*/
private function add_schema_type_xrefs( &$schema_types ) { // Pass by reference is OK.
$thing =& $schema_types[ 'thing' ]; // Quick ref variable for the 'thing' array.
/*
* Thing > Intangible > Enumeration.
*/
$thing[ 'intangible' ][ 'enumeration' ][ 'specialty' ][ 'medical.specialty' ] =&
$thing[ 'intangible' ][ 'enumeration' ][ 'medical.enumeration' ][ 'medical.specialty' ];
$thing[ 'intangible' ][ 'service' ][ 'service.financial.product' ][ 'payment.card' ] =&
$thing[ 'intangible' ][ 'enumeration' ][ 'payment.method' ][ 'payment.card' ];
/*
* Thing > Organization > Educational Organization.
*/
$thing[ 'organization' ][ 'educational.organization' ] =& $thing[ 'place' ][ 'civic.structure' ][ 'educational.organization' ];
/*
* Thing > Organization > Local Business.
*/
$thing[ 'organization' ][ 'local.business' ] =& $thing[ 'place' ][ 'local.business' ];
}
}
}