p =& $plugin; if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } /* * Instantiate the WpssoOpenGraphNS class object. */ if ( ! class_exists( 'WpssoOpenGraphNS' ) ) { require_once WPSSO_PLUGINDIR . 'lib/opengraph-ns.php'; } $this->ns = new WpssoOpenGraphNS( $plugin ); $this->p->util->add_plugin_filters( $this, array( 'plugin_image_sizes' => 1, ) ); $this->p->util->add_plugin_filters( $this, array( 'get_post_options' => 3, ), PHP_INT_MAX ); } public function filter_plugin_image_sizes( array $sizes ) { $sizes[ 'og' ] = array( // Option prefix. 'name' => 'opengraph', 'label_transl' => _x( 'Open Graph (Facebook and oEmbed)', 'option label', 'wpsso' ), ); return $sizes; } public function filter_get_post_options( array $md_opts, $post_id, array $mod ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } if ( is_admin() ) { // Keep processing on the front-end to a minimum. /* * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and * the Schema type, check for a possible hard-coded Open Graph type. */ $md_opts = $this->maybe_update_post_og_type( $md_opts, $post_id, $mod ); } return $md_opts; } /* * Returns the open graph type id. */ public function get_mod_og_type_id( array $mod, $use_md_opts = true ) { return $this->get_mod_og_type( $mod, $get_id = true, $use_md_opts ); } /* * Returns the open graph namespace. */ public function get_mod_og_type_ns( array $mod, $use_md_opts = true ) { return $this->get_mod_og_type( $mod, $get_id = false, $use_md_opts ); } /* * Returns the open graph type id by default. * * Example: article, product, place, etc. * * Use $get_id = false to return the open graph namespace instead of the ID. */ public function get_mod_og_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 og 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; $og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ]; /* * Maybe get a custom open graph type id from the post, term, or user meta. */ if ( $use_md_opts ) { if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) { $type_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'og_type' ); // Returns null if index key not found. if ( empty( $type_id ) || $type_id === 'none' || empty( $og_type_ns[ $type_id ] ) ) { // Check for an invalid type id. $type_id = null; } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'custom type id = ' . $type_id ); } } } $is_custom = empty( $type_id ) ? false : true; if ( ! $is_custom ) { // No custom open graph 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_og_type_id( 'home_page' ); } else $type_id = $this->get_og_type_id( 'home_posts' ); } elseif ( $mod[ 'is_comment' ] ) { if ( is_numeric( $mod[ 'comment_rating' ] ) ) { $type_id = $this->get_og_type_id( 'comment_review' ); } elseif ( $mod[ 'comment_parent' ] ) { $type_id = $this->get_og_type_id( 'comment_reply' ); } else $type_id = $this->get_og_type_id( 'comment' ); } elseif ( $mod[ 'is_post' ] ) { if ( ! empty( $mod[ 'post_type' ] ) && is_string( $mod[ 'post_type' ] ) ) { // Not false or empty string. if ( $mod[ 'is_post_type_archive' ] ) { // The post ID may be 0. $type_id = $this->get_og_type_id( 'pta_' . $mod[ 'post_type' ] ); if ( empty( $type_id ) ) { // Just in case. $type_id = $this->get_og_type_id( 'archive_page' ); } } else { $type_id = $this->get_og_type_id( $mod[ 'post_type' ] ); if ( empty( $type_id ) ) { // Just in case. $type_id = $this->get_og_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_og_type_id( 'tax_' . $mod[ 'tax_slug' ] ); } if ( empty( $type_id ) ) { // Just in case. $type_id = $this->get_og_type_id( 'archive_page' ); } } elseif ( $mod[ 'is_user' ] ) { $type_id = $this->get_og_type_id( 'user_page' ); } elseif ( $mod[ 'is_search' ] ) { $type_id = $this->get_og_type_id( 'search_page' ); } elseif ( $mod[ 'is_archive' ] ) { $type_id = $this->get_og_type_id( 'archive_page' ); } if ( empty( $type_id ) ) { // Just in case. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'unable to determine og type id (using default)' ); } $type_id = 'website'; } } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'og type id before filter = ' . $type_id ); } $type_id = apply_filters( 'wpsso_og_type', $type_id, $mod, $is_custom ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'og type id after filter = ' . $type_id ); } $get_value = false; if ( empty( $type_id ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning false: og type id is empty' ); } } elseif ( 'none' === $type_id ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning false: og type id is disabled' ); } } elseif ( ! isset( $og_type_ns[ $type_id ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning false: og type id ' . $type_id . ' is unknown' ); } } elseif ( ! $get_id ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning og type id namespace: ' . $og_type_ns[ $type_id ] ); } $get_value = $og_type_ns[ $type_id ]; } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning og type id: ' . $type_id ); } $get_value = $type_id; } /* * Optimize and cache post/term/user og type values. */ if ( $cache_salt ) { $local_cache[ $cache_salt ] = $get_value; } return $get_value; } /* * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names. * * $size_name is passed as-is to WpssoMedia->get_all_images(). */ public function get_array( array $mod, $size_names = 'opengraph', $md_pre = 'og' ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $max_nums = $this->p->util->get_max_nums( $mod ); if ( $this->p->debug->enabled ) { $this->p->debug->mark( 'applying filters "wpsso_og_seed"' ); // Begin timer. } /* * 'wpsso_og_seed' is hooked by e-commerce modules to provide product meta tags. */ $mt_og = apply_filters( 'wpsso_og_seed', SucomUtil::get_mt_og_seed(), $mod ); if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'og_seed', $mt_og ); $this->p->debug->mark( 'applying filters "wpsso_og_seed"' ); // End timer. } /* * Facebook app id meta tag. */ if ( ! isset( $mt_og[ 'fb:app_id' ] ) ) { $mt_og[ 'fb:app_id' ] = $this->p->options[ 'fb_app_id' ]; } /* * Current ISO 8601 date and time (non-standard / internal meta tag). */ $mt_og[ 'og:time' ] = gmdate( 'c' ); /* * Type id meta tag. */ if ( ! isset( $mt_og[ 'og:type' ] ) ) { $mt_og[ 'og:type' ] = $this->get_mod_og_type_id( $mod ); } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:type already defined = ' . $mt_og[ 'og:type' ] ); } $type_id = $mt_og[ 'og:type' ]; /* * URL meta tag. */ if ( ! isset( $mt_og[ 'og:url' ] ) ) { $mt_og[ 'og:url' ] = $this->p->util->get_canonical_url( $mod ); } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:url already defined = ' . $mt_og[ 'og:url' ] ); } /* * Redirect URL meta tag (non-standard / internal meta tag). */ $mt_og[ 'og:redirect_url' ] = $this->p->util->get_redirect_url( $mod ); /* * Locale meta tag. */ if ( ! isset( $mt_og[ 'og:locale' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'calling get_fb_locale()' ); } $mt_og[ 'og:locale' ] = $this->get_fb_locale( $mod ); } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:locale already defined = ' . $mt_og[ 'og:locale' ] ); } /* * Site name meta tag. */ if ( ! isset( $mt_og[ 'og:site_name' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting site name for og:site_name meta tag' ); } $mt_og[ 'og:site_name' ] = SucomUtilWP::get_site_name( $this->p->options, $mod ); // localized } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:site_name already defined = ' . $mt_og[ 'og:site_name' ] ); } /* * Title meta tag. */ if ( ! isset( $mt_og[ 'og:title' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting title for og:title meta tag' ); } $mt_og[ 'og:title' ] = $this->p->page->get_title( $mod, $md_key = 'og_title', $max_len = 'og_title' ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:title value = ' . $mt_og[ 'og:title' ] ); } } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:title already defined = ' . $mt_og[ 'og:title' ] ); } /* * Description meta tag. */ if ( ! isset( $mt_og[ 'og:description' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting description for og:description meta tag' ); } $mt_og[ 'og:description' ] = $this->p->page->get_description( $mod, $md_key = 'og_desc', $max_len = 'og_desc', $num_hashtags = true ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:description value = ' . $mt_og[ 'og:description' ] ); } } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'og:description already defined = ' . $mt_og[ 'og:description' ] ); } /* * Updated date / time meta tag. */ if ( ! isset( $mt_og[ 'og:updated_time' ] ) ) { if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) { // ISO 8601 date or false. $mt_og[ 'og:updated_time' ] = $mod[ 'post_modified_time' ]; } } /* * Get all videos. * * Call before getting all images to find / use preview images. */ if ( ! isset( $mt_og[ 'og:video' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting videos for og:video meta tag' ); } if ( $max_nums[ 'og_vid_max' ] > 0 ) { $mt_og[ 'og:video' ] = $this->p->media->get_all_videos( $max_nums[ 'og_vid_max' ], $mod, $md_pre ); if ( empty( $mt_og[ 'og:video' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'no videos returned' ); } unset( $mt_og[ 'og:video' ] ); } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'removing video images to avoid duplicates' ); } foreach ( $mt_og[ 'og:video' ] as $key => $mt_single ) { if ( ! is_array( $mt_single ) ) { // Just in case. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'video ignored: $mt_single is not an array' ); } continue; } $mt_og[ 'og:video' ][ $key ] = SucomUtil::preg_grep_keys( '/^og:image/', $mt_single, $invert = true ); } } } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'videos disabled: maximum videos is 0 or less' ); } } /* * Get all images. */ if ( ! isset( $mt_og[ 'og:image' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting images for og:image meta tag' ); } if ( $max_nums[ 'og_img_max' ] > 0 ) { $mt_og[ 'og:image' ] = $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre ); if ( empty( $mt_og[ 'og:image' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'no images returned' ); } unset( $mt_og[ 'og:image' ] ); } } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'skipped getting images: maximum images is 0 or less' ); } } } /* * Pre-define some basic open graph meta tags for this og:type. If the meta tag has an associated meta * option name, then read it's value from the meta options. */ if ( $this->p->debug->enabled ) { $this->p->debug->log( 'checking og_type_mt array for known meta tags and md options' ); } /* * An array of Open Graph types, their meta tags, and their associated metadata keys. */ if ( isset( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) { // Check if og:type is known. /* * Optimize and call get_options() only once. Returns an empty string if no meta found. */ if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) { $md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_key = false, $filter_opts = true, $merge_defs = true ); } else $md_opts = array(); /* * Add post/term/user meta data to the Open Graph meta tags. */ $this->add_data_og_type_md( $mt_og, $type_id, $md_opts ); } /* * If the module is a post object, define the author, publishing date, etc. These values may still be used * by other non-article filters, and if the og:type is not an article, the meta tags will be sanitized (ie. * non-valid meta tags removed) at the end of WpssoHead->get_head_array(). */ if ( ! isset( $mt_og[ 'article:author' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'getting names / URLs for article:author meta tags' ); } $mt_og[ 'article:author' ] = array(); if ( $mod[ 'is_post' ] ) { if ( $mod[ 'post_author' ] ) { $mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'post_author' ], 'display_name' ); $mt_og[ 'article:author' ] = $this->p->user->get_authors_websites( $mod[ 'post_author' ], $meta_key = 'facebook' ); } if ( $mod[ 'post_coauthors' ] ) { $og_profile_urls = $this->p->user->get_authors_websites( $mod[ 'post_coauthors' ], $meta_key = 'facebook' ); $mt_og[ 'article:author' ] = array_merge( $mt_og[ 'article:author' ], $og_profile_urls ); } } elseif ( $mod[ 'is_comment' ] ) { if ( $mod[ 'comment_author' ] ) { $mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'comment_author' ], 'display_name' ); $mt_og[ 'article:author' ] = $this->p->user->get_authors_websites( $mod[ 'comment_author' ], $meta_key = 'facebook' ); } elseif ( $mod[ 'comment_author_name' ] ) { $mt_og[ 'article:author:name' ] = $mod[ 'comment_author_name' ]; } } } if ( ! isset( $mt_og[ 'article:publisher' ] ) ) { $mt_og[ 'article:publisher' ] = SucomUtilOptions::get_key_value( 'fb_publisher_url', $this->p->options, $mod ); } if ( ! isset( $mt_og[ 'article:tag' ] ) ) { $mt_og[ 'article:tag' ] = $this->p->page->get_tag_names( $mod ); } if ( ! isset( $mt_og[ 'article:published_time' ] ) ) { if ( $mod[ 'is_post' ] && $mod[ 'post_time' ] ) { // ISO 8601 date or false. switch ( $mod[ 'post_status' ] ) { case 'auto-draft': case 'draft': case 'future': case 'inherit': // Post revision. case 'pending': case 'trash': if ( $this->p->debug->enabled ) { $this->p->debug->log( 'skipping article published time for post status ' . $mod[ 'post_status' ] ); } break; case 'expired': // Previously published. case 'private': case 'publish': default: // Any other post status. $mt_og[ 'article:published_time' ] = $mod[ 'post_time' ]; break; } } elseif ( $mod[ 'is_comment' ] && $mod[ 'comment_time' ] ) { // ISO 8601 date or false. $mt_og[ 'article:published_time' ] = $mod[ 'comment_time' ]; } } if ( ! isset( $mt_og[ 'article:modified_time' ] ) ) { if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) { // ISO 8601 date or false. $mt_og[ 'article:modified_time' ] = $mod[ 'post_modified_time' ]; } } if ( ! isset( $mt_og[ 'article:reading_mins' ] ) ) { $mt_og[ 'article:reading_mins' ] = $this->p->page->get_reading_mins( $mod ); } if ( ! empty( $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ] ) ) { $og_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ]; // Example: https://ogp.me/ns/product# $filter_name = 'wpsso_og_data_' . SucomUtil::sanitize_hookname( $og_ns ); $mt_og = apply_filters( $filter_name, $mt_og, $mod ); } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'applying filters "wpsso_og"' ); } $mt_og = apply_filters( 'wpsso_og', $mt_og, $mod ); /* * Expand inline variables. */ $mt_og = $this->p->util->inline->replace_variables( $mt_og, $mod ); if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'mt_og', $mt_og ); } return $mt_og; } public function get_og_type_id( $opt_suffix, $default_id = 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; } $og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ]; $opt_key = 'og_type_for_' . $opt_suffix; $type_id = isset( $this->p->options[ $opt_key ] ) ? $this->p->options[ $opt_key ] : $default_id; if ( empty( $type_id ) || 'none' === $type_id || empty( $og_type_ns[ $type_id ] ) ) { // Invalid type id. if ( empty( $default_id ) || 'none' === $default_id || empty( $og_type_ns[ $default_id ] ) ) { // Invalid default id. return false; } return $default_id; } return $type_id; } public function get_og_types() { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } return $this->p->cf[ 'head' ][ 'og_type_ns_compat' ]; } public function get_og_types_select() { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $og_type_ns = $this->get_og_types(); $select = array(); foreach ( $og_type_ns as $type_id => $type_ns ) { $type_url = preg_replace( '/(^.*\/\/|#$)/', '', $type_ns ); $type_name = preg_replace( '/(^.*\/ns\/|#$)/U', '', $type_url ); switch ( $this->p->options[ 'plugin_og_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_url; 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 optional and customized locale value for the og:locale meta tag. * * $mixed = 'default' | 'current' | post ID | $mod array * * See WpssoOpenGraph->get_array(). * See WpssoOptions->get_defaults(). */ public function get_fb_locale( $mixed = 'current', $use_opts = true ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } /* * Maybe get an alternate Facebook locale from the plugin settings. */ if ( $use_opts ) { $key_locale = SucomUtilOptions::get_key_locale( 'fb_locale', $this->p->options, $mixed ); if ( ! empty( $this->p->options[ $key_locale ] ) ) { // Maybe use an alternate locale value. $fb_locale = $this->p->options[ $key_locale ]; if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning fb locale "' . $fb_locale . '" from ' . $key_locale . ' options key' ); } return $fb_locale; } } /* * Maybe return the Facebook equivalent for this locale. */ if ( $this->p->debug->enabled ) { $this->p->debug->log( 'calling get_schema_lang()' ); } $schema_lang = $this->p->schema->get_schema_lang( $mixed ); $fb_languages = SucomUtil::get_publisher_languages( 'facebook' ); /* * Fix known exceptions. */ switch ( $schema_lang ) { case 'en_UK': $schema_lang = 'en_GB'; break; case 'de_DE_formal': $schema_lang = 'de_DE'; break; } if ( ! empty( $fb_languages[ $schema_lang ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning schema language "' . $schema_lang . '"' ); } return $schema_lang; } /* * Maybe fallback to the default locale. */ $def_locale = $this->p->schema->get_schema_lang( 'default' ); if ( ! empty( $fb_languages[ $def_locale ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning default locale "' . $def_locale . '"' ); } return $def_locale; } /* * Fallback to en_US. */ if ( $this->p->debug->enabled ) { $this->p->debug->log( 'returning fallback locale "en_US"' ); } return 'en_US'; } /* * Since WPSSO Core v15.0.0. * * See WpssoCmcfActions->check_product_image_urls(). * See WpssoCmcfXml->add_product_images(). * See WpssoGmfActions->check_product_image_urls(). * See WpssoGmfXml->add_product_images(). */ public function get_product_retailer_item_image_urls( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) { $image_urls = array(); $mt_images = $this->get_product_retailer_item_images( $mt_single, $size_names, $md_pre ); if ( is_array( $mt_images ) ) { // Just in case. foreach ( $mt_images as $mt_image ) { if ( $image_url = SucomUtil::get_first_og_image_url( $mt_image ) ) { $image_urls[] = $image_url; } } } return $image_urls; } /* * Since WPSSO Core v15.0.0. * * WpssoOpenGraph->get_product_retailer_item_image_urls(). */ public function get_product_retailer_item_images( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) { if ( isset( $mt_single[ 'og:image' ] ) && is_array( $mt_single[ 'og:image' ] ) ) { // Nothing to do. return $mt_single[ 'og:image' ]; } elseif ( ! $mod = $this->get_product_retailer_item_mod( $mt_single ) ) { // Returns false or array. if ( $wpsso->debug->enabled ) { $wpsso->debug->log( 'returning early: no post id for retailer item id' ); } return array(); } $max_nums = $this->p->util->get_max_nums( $mod, 'og' ); // $mod must be an array. return $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre ); } /* * Since WPSSO Core v15.0.0. * * See WpssoCmcfActions->check_product_image_urls(). * See WpssoGmfActions->check_product_image_urls(). * See WpssoOpenGraph->get_product_retailer_item_images(). * See WpssoSchema::add_offers_aggregate_data_mt(). * See WpssoSchema::add_offers_data_mt(). * See WpssoSchema::add_variants_data_mt(). */ public function get_product_retailer_item_mod( array $mt_single ) { if ( ! empty( $mt_single[ 'product:retailer_item_id' ] ) && is_numeric( $mt_single[ 'product:retailer_item_id' ] ) ) { $mod = $this->p->post->get_mod( $mt_single[ 'product:retailer_item_id' ] ); return empty( $mod[ 'id' ] ) ? false : $mod; // Just in case. } return false; } /* * Returns a string. * * Used for the 'product:retailer_category' meta tag value. */ public function get_product_retailer_category( $mod ) { $retailer_categories = $this->get_product_retailer_categories( $mod, $lists_max = 1 ); return isset( $retailer_categories[ 0 ] ) ? $retailer_categories[ 0 ] : ''; } /* * Returns an array of strings. */ public function get_product_retailer_categories( $mod, $lists_max = 1 ) { /* * Returns an associative array of term IDs and their names or objects. * * If the custom primary or default term ID exists in the post terms, it will be moved to the top. */ $post_terms = $this->p->post->get_primary_terms( $mod, $mod[ 'post_primary_tax_slug' ], $output = 'objects' ); $category_names = array(); foreach ( $post_terms as $parent_term_obj ) { $parent_term_id = $parent_term_obj->term_id; $ancestor_ids = get_ancestors( $parent_term_id, $mod[ 'post_primary_tax_slug' ], $resource_type = 'taxonomy' ); if ( empty( $ancestor_ids ) || ! is_array( $ancestor_ids ) ) { $ancestor_ids = array( $parent_term_id ); // Just do the parent term. } else { $ancestor_ids = array_reverse( $ancestor_ids ); // Add ancestors in reverse order. $ancestor_ids[] = $parent_term_id; // Add parent term last. } foreach ( $ancestor_ids as $term_id ) { $term_mod = $this->p->term->get_mod( $term_id ); /* * Use $title_sep = false to avoid adding term parent names in the term title. * * $md_key = 'schema_title_bc' will use array( 'schema_title_bc', 'schema_title_alt', 'schema_title', 'seo_title' ) */ $category_names[ $parent_term_id ][ $term_id ] = $this->p->page->get_title( $term_mod, $md_key = 'schema_title_bc', $max_len = 'schema_title_bc', $title_sep = false ); } } if ( $lists_max ) { $category_names = array_slice( $category_names, 0, $lists_max ); } $retailer_categories = array(); foreach ( $category_names as $parent_term_id => $term_ids ) { $retailer_categories[] = implode( ' > ', $term_ids ); } return $retailer_categories; } /* * Returns an array of strings. */ public function get_product_awards( array $mod ) { $awards = array(); if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) { $md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ] ); if ( is_array( $md_opts ) ) { // Just in case. $awards = SucomUtil::get_multi_values( $md_opts, 'product_award' ); } } $awards = apply_filters( 'wpsso_og_product_awards', $awards, $mod ); return $awards; } /* * Called by WpssoHead->get_head_array() before merging all meta tag arrays. * * Unset mis-matched og_type meta tags using the 'og_type_mt' array as a reference. For example, remove all * 'article' meta tags if the og_type is 'website'. Removing only known meta tags (using the 'og_type_mt' array as * a reference) protects internal meta tags that may be used later by WpssoHead->extract_head_info(). * * The 'og_content_map' array is also checked for Schema values that need to be swapped for simpler Open Graph meta * tag values. */ public function sanitize_mt_array( array $mt_og ) { /* * Array of meta tags to allow, reject, and map. */ static $og_allow = array(); static $og_reject = array(); static $content_map = array(); static $local_recursion = 0; if ( $local_recursion > 32 ) { // Just in case. return $mt_og; } if ( 0 === $local_recursion ) { // Define the static variables once. /* * The og:type is only needed when first run, to define the allow, reject, and map arrays. */ if ( empty( $mt_og[ 'og:type' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'exiting early: og:type is empty and required' ); } return $mt_og; } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'setting og allow, reject, and content map arrays' ); } $og_type = $mt_og[ 'og:type' ]; /* * An array of Open Graph types, their meta tags, and their associated metadata keys. */ foreach ( $this->p->cf[ 'head' ][ 'og_type_mt' ] as $type_id => $og_type_mt_md ) { /* * An array of meta tags and their associated metadata keys. */ foreach ( $og_type_mt_md as $mt_name => $md_key ) { if ( $og_type === $type_id ) { $og_allow[ $mt_name ] = true; // Mark meta tag as allowed. /* * If we have a content map for the meta tag, save the content mapping array. * * 'product:availability' => array( * 'https://schema.org/BackOrder' => 'available for order', * 'https://schema.org/Discontinued' => 'discontinued', * 'https://schema.org/InStock' => 'in stock', * 'https://schema.org/InStoreOnly' => 'in stock', * 'https://schema.org/LimitedAvailability' => 'in stock', * 'https://schema.org/OnlineOnly' => 'in stock', * 'https://schema.org/OutOfStock' => 'out of stock', * 'https://schema.org/PreOrder' => 'available for order', * 'https://schema.org/PreSale' => 'available for order', * 'https://schema.org/SoldOut' => 'out of stock', * ), */ if ( ! empty( $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ] ) ) { /* * Save the content mapping array. */ $content_map[ $mt_name ] = $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ]; } } else $og_reject[ $mt_name ] = true; // Mark meta tag as disallowed. } } foreach ( array( 'product:offers', 'product:variants' ) as $mt_name ) { if ( ! empty( $mt_og[ $mt_name ] ) && is_array( $mt_og[ $mt_name ] ) ) { foreach ( $mt_og[ $mt_name ] as $num => &$mt_single ) { // Allow changes to the variation array. unset( $mt_single[ 'product:brand' ] ); // Allow only a single brand value (in the parent product). foreach ( $mt_single as $mt_single_name => $mt_single_value ) { /* * Avoid duplicate values (like prices) between the main product and its variations. */ if ( isset( $mt_og[ $mt_single_name ] ) && $mt_og[ $mt_single_name ] === $mt_single_value ) { /* * Duplicate prices and values are removed first. Check if * the currency or units meta tag needs to be removed as * well (ie. if the price or value does not exist). */ if ( true === $this->have_currency_units_value( $mt_og, $mt_single_name ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' kept' ); } continue; } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' removed' ); } unset( $mt_og[ $mt_single_name ] ); } } } } } } /* * Check the meta tag names and their values. */ foreach ( $mt_og as $key => $val ) { if ( ! empty( $og_allow[ $key ] ) ) { // Meta tag is allowed - check it's value. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'allowed meta tag ' . $key ); } /* * If we have a matching value in the content map, then assign the mapped value. * * 'https://schema.org/BackOrder' to 'available for order' for example. */ if ( isset( $content_map[ $key ][ $val ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'mapping meta tag ' . $key . ' = ' . $content_map[ $key ][ $val ] ); } $mt_og[ $key ] = $content_map[ $key ][ $val ]; /* * If this is a currency or units meta tag without a price or value, then set the meta tag to null. */ } elseif ( false === $this->have_currency_units_value( $mt_og, $key ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'setting meta tag ' . $key . ' = null' ); } $mt_og[ $key ] = null; } } elseif ( ! empty( $og_reject[ $key ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'rejected meta tag ' . $key ); } unset( $mt_og[ $key ] ); } elseif ( is_array( $val ) ) { if ( $this->p->debug->enabled ) { if ( is_string( $key ) ) { $this->p->debug->log( 'sanitizing meta tag ' . $key . ' array' ); } } $local_recursion++; $mt_og[ $key ] = $this->sanitize_mt_array( $val ); $local_recursion--; } } return $mt_og; } /* * Add post/term/user meta data to the Open Graph meta tags. */ public function add_data_og_type_md( array &$mt_og, $type_id, array $md_opts ) { // Pass by reference is OK. if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } /* * An array of Open Graph types, their meta tags, and their associated metadata keys. */ if ( empty( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) { // Just in case. return; } $og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ]; if ( $this->p->debug->enabled ) { $this->p->debug->log( 'loading og_type_mt array for type id ' . $type_id ); } $md_keys_multi = WpssoConfig::get_md_keys_multi(); // Uses a local cache. foreach ( $og_type_mt_md as $mt_name => $md_key_pre ) { if ( empty( $md_key_pre ) ) { // Most of the metadata keys are empty. continue; } elseif ( false !== strpos( $mt_name, ':units' ) ) { // Define units when we define the value. continue; } $is_multi = empty( $md_keys_multi[ $md_key_pre ] ) ? false : true; $multi_keys = $is_multi ? array_keys( SucomUtil::preg_grep_keys( '/^' . $md_key_pre . '_[0-9]+$/', $md_opts ) ) : array( $md_key_pre ); foreach ( $multi_keys as $md_key ) { $values = array(); /* * Use custom value(s) if available. Returns true or false. */ if ( ! $this->have_og_type_md_values( $values, $type_id, $md_opts, $mt_name, $md_key ) ) { $def_md_key = $this->get_def_md_key( $md_key ); if ( isset( $mt_og[ $mt_name ] ) ) { // Meta tag exists and is not null. if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_name . ' value kept = ' . $mt_og[ $mt_name ] ); } } elseif ( ! empty( $def_md_key ) ) { // Add default value from the plugin settings. if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_name . ' from options = ' . $this->p->options[ $def_md_key ] ); } $values[ $mt_name ] = $this->p->options[ $def_md_key ]; } else { // Add missing meta tag with null value. if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_name . ' = null' ); } $values[ $mt_name ] = null; // Use null so isset() returns false. } } foreach ( $values as $mt_name => $val ) { // Can be an empty array. if ( 'none' === $val ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'setting ' . $mt_name . ' = null for value none' ); } $mt_og[ $mt_name ] = null; } elseif ( $is_multi ) { $mt_og[ $mt_name ][] = $val; } else $mt_og[ $mt_name ] = $val; } } } } /* * Check for a default value from the settings. * * Open Graph defaults have an 'og_def_' prefix, except for 'product_currency'. * * Schema defaults have a 'schema_def_' prefix. */ public function get_def_md_key( $md_key ) { $def_md_key = null; if ( 'product_currency' === $md_key ) { if ( isset( $this->p->options[ 'og_def_currency' ] ) ) { // Just in case. $def_md_key = 'og_def_currency'; } } elseif ( isset( $this->p->options[ 'og_def_' . $md_key ] ) ) { $def_md_key = 'og_def_' . $md_key; } elseif ( isset( $this->p->options[ 'schema_def_' . $md_key ] ) ) { $def_md_key = 'schema_def_' . $md_key; } return $def_md_key; } /* * If we have a GTIN number, try to improve the assigned property name. * * Pass $mt_og by reference to modify the array directly. * * A similar method exists as WpssoSchema::check_prop_value_gtin(). */ public static function check_mt_value_gtin( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK. $wpsso =& Wpsso::get_instance(); if ( $wpsso->debug->enabled ) { $wpsso->debug->log( 'checking ' . $mt_pre . ' gtin value' ); } if ( isset( $mt_og[ $mt_pre . ':gtin' ] ) ) { /* * The value may come from a custom field, so trim it, just in case. */ $mt_og[ $mt_pre . ':gtin' ] = preg_replace( '/\s+/', '', $mt_og[ $mt_pre . ':gtin' ] ); $gtin_len = strlen( $mt_og[ $mt_pre . ':gtin' ] ); switch ( $gtin_len ) { case 13: if ( empty( $mt_og[ $mt_pre . ':ean' ] ) ) { $mt_og[ $mt_pre . ':ean' ] = $mt_og[ $mt_pre . ':gtin' ]; } break; case 12: if ( empty( $mt_og[ $mt_pre . ':upc' ] ) ) { $mt_og[ $mt_pre . ':upc' ] = $mt_og[ $mt_pre . ':gtin' ]; } break; } } } public static function check_mt_value_price( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK. $wpsso =& Wpsso::get_instance(); if ( $wpsso->debug->enabled ) { $wpsso->debug->log( 'checking ' . $mt_pre . ' price value' ); } $def_currency_key = $mt_pre . ':price:currency'; $def_currency = empty( $mt_og[ $def_currency_key ] ) ? $wpsso->options[ 'og_def_currency' ] : $mt_og[ $def_currency_key ]; $min_adv_price_key = $mt_pre . ':min_advert_price:amount'; $min_adv_price = empty( $mt_og[ $min_adv_price_key ] ) ? 0 : $mt_og[ $min_adv_price_key ]; foreach ( array( 'min_advert_price', 'original_price', 'pretax_price', 'price', 'sale_price', 'shipping_cost' ) as $price_name ) { $amount_key = $mt_pre . ':' . $price_name . ':amount'; $currency_key = $mt_pre . ':' . $price_name . ':currency'; if ( isset( $mt_og[ $amount_key ] ) ) { if ( is_numeric( $mt_og[ $amount_key ] ) ) { // Allow for price of 0. if ( $min_adv_price && $mt_og[ $amount_key ] < $min_adv_price ) { $mt_og[ $mt_pre . ':price:type' ] = 'https://schema.org/MinimumAdvertisedPrice'; $mt_og[ $amount_key ] = $min_adv_price; } if ( empty( $mt_og[ $currency_key ] ) ) { $mt_og[ $currency_key ] = $def_currency; } } else { if ( ! empty( $mt_og[ $amount_key ] ) ) { // Non-empty string, array, etc. if ( $wpsso->debug->enabled ) { $wpsso->debug->log( 'invalid ' . $amount_key . ' value = ' . print_r( $mt_og[ $amount_key ], true ) ); } } $mt_og[ $amount_key ] = null; $mt_og[ $currency_key ] = null; } } } } public static function check_mt_value_energy_efficiency( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK. $wpsso =& Wpsso::get_instance(); if ( $wpsso->debug->enabled ) { $wpsso->debug->log( 'checking ' . $mt_pre . ' energy efficiency value' ); } if ( empty( $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) || 'none' === $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) { $mt_og[ $mt_pre . ':energy_efficiency:value' ] = null; $mt_og[ $mt_pre . ':energy_efficiency:min_value' ] = null; $mt_og[ $mt_pre . ':energy_efficiency:max_value' ] = null; } } private function have_currency_units_value( $mt_og, $mt_key ) { if ( preg_match( '/^(.*:)(currency|units)$/', $mt_key, $matches ) ) { switch ( $matches[ 2 ] ) { case 'currency': $mt_value_key = $matches[ 1 ] . 'amount'; break; case 'units': $mt_value_key = $matches[ 1 ] . 'value'; break; default: $mt_value_key = null; break; } if ( $mt_value_key ) { return isset( $mt_og[ $mt_value_key ] ) && '' !== $mt_og[ $mt_value_key ] ? true : false; } } return null; } /* * Use custom value(s) if available. Returns true or false. */ private function have_og_type_md_values( array &$values, $type_id, array $md_opts, $mt_name, $md_key ) { // Pass by reference is OK. if ( empty( $md_key ) || ! isset( $md_opts[ $md_key ] ) || '' === $md_opts[ $md_key ] ) { return false; } /* * Check for meta tags that require a unit value and may need to be converted. */ if ( false !== strpos( $mt_name, ':value' ) && preg_match( '/^(.*):value$/', $mt_name, $mt_match ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] ); } $values[ $mt_name ] = $md_opts[ $md_key ]; /* * Check if the value meta tag needs a units meta tag. */ $mt_units_name = $mt_match[ 1 ] . ':units'; /* * An array of Open Graph types, their meta tags, and their associated metadata keys. */ $og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ]; if ( isset( $og_type_mt_md[ $mt_units_name ] ) ) { // Value needs a units meta tag. $md_units_key = $og_type_mt_md[ $mt_units_name ]; // An empty string or metadata options key. $values[ $mt_units_name ] = $unit_text = WpssoSchema::get_unit_text( $md_key ); if ( ! empty( $md_opts[ $md_units_key ] ) ) { // Custom unit text found. if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_units_name . ' from metadata = ' . $md_opts[ $md_units_key ] ); } $values[ $mt_units_name ] = $md_opts[ $md_units_key ]; } /* * Since WPSSO Core v14.0.0. * * Check if we need to convert the value. */ if ( $values[ $mt_units_name ] !== $unit_text ) { $unit_value = WpssoUtilUnits::get_convert( $values[ $mt_name ], $unit_text, $values[ $mt_units_name ] ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'converted ' . $values[ $mt_name ] . ' ' . $values[ $mt_units_name ] . ' to ' . $unit_value . ' ' . $unit_text ); } $values[ $mt_name ] = $unit_value; $values[ $mt_units_name ] = $unit_text; } } return true; } /* * If the value does not need to be converted, then add the custom metadata value as-is. */ if ( $this->p->debug->enabled ) { $this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] ); } $values[ $mt_name ] = $md_opts[ $md_key ]; return true; } /* * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and the Schema type, * check for a possible hard-coded Open Graph type. */ private function maybe_update_post_og_type( array $md_opts, $post_id, array $mod ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } if ( empty( $md_opts[ 'og_type:disabled' ] ) ) { // Just in case. /* * Check if the post type matches a pre-defined Open Graph type. * * For example, a post type of 'organization' would return 'website' for the Open Graph type. * * Returns false or an Open Graph type string. */ if ( $og_type = $this->p->post->get_post_type_og_type( $mod ) ) { $md_opts[ 'og_type' ] = $og_type; $md_opts[ 'og_type:disabled' ] = true; } else { /* * Use the saved Schema type or get the default Schema type. */ if ( isset( $md_opts[ 'schema_type' ] ) ) { $type_id = $md_opts[ 'schema_type' ]; } else $type_id = $this->p->schema->get_mod_schema_type_id( $mod, $use_md_opts = false ); /* * Check if the Schema type matches a pre-defined Open Graph type. */ if ( $og_type = $this->p->schema->get_schema_type_og_type( $type_id ) ) { $md_opts[ 'og_type' ] = $og_type; $md_opts[ 'og_type:disabled' ] = true; } } } return $md_opts; } } }