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[][] = '' . "\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 '' . "\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, Google does not allow reviews for the Schema %2$s type.', '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' ]; } } }