p =& $plugin; if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } /* * If any custom modifications are required to the WP_Query 'query_vars', they should be done before the * 'wp_head' action is triggered. The WpssoHead->show_head() method calls WpssoPage->get_mod() to determine * the current WordPress object (comment, post, term, or user), if any, and saves the 'query_vars' value * for WordPress archive queries. */ add_action( 'wp_head', array( $this, 'show_head' ), WPSSO_HEAD_PRIORITY ); /* * If an AMP plugin is active, hook 'amp_post_template_head' to add head markup for AMP pages. */ if ( ! empty( $this->p->avail[ 'amp' ][ 'any' ] ) ) { add_action( 'amp_post_template_head', array( $this, 'show_head' ), WPSSO_HEAD_PRIORITY ); if ( $this->p->avail[ 'p' ][ 'schema' ] ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'disabling amp_post_template_metadata' ); } add_filter( 'amp_post_template_metadata', '__return_empty_array', PHP_INT_MAX ); } } /* * Maybe do a 301 redirect. */ add_action( 'template_redirect', array( $this, 'maybe_redirect_url' ), PHP_INT_MIN ); } public function maybe_redirect_url() { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } if ( is_admin() || is_preview() || is_customize_preview() ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'exiting early: is admin, preview, or customize preview' ); } return; /* * WpssoUtil->is_redirect_disabled() returns true if: * * - An SEO plugin is active. * - The 'wpsso_redirect_disabled' filter returns true. */ } elseif ( $this->p->util->is_redirect_disabled() ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'exiting early: redirect is disabled' ); } return; } $use_post = apply_filters( 'wpsso_use_post', false ); $url = $this->p->util->get_redirect_url( $use_post ); if ( $url ) { do_action( 'wpsso_before_redirect', $url ); $redirect_code = absint( apply_filters( 'wpsso_redirect_status_code', 301 ) ); if ( wp_safe_redirect( $url, $redirect_code ) ) { exit; } } } public function add_vary_user_agent_header( $headers ) { $headers [ 'Vary' ] = 'User-Agent'; return $headers; } /* * Called by 'wp_head' and 'amp_post_template_head' actions. * * If any custom modifications are required to the WP_Query 'query_vars', they should be done before the 'wp_head' * action is triggered. The WpssoHead->show_head() method calls WpssoPage->get_mod() to determine the current * WordPress object (comment, post, term, or user), if any, and saves the 'query_vars' value for WordPress archive * queries. */ public function show_head() { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } if ( is_admin() || is_preview() || is_customize_preview() ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'exiting early: is admin, preview, or customize preview' ); } return; } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'required call to WpssoPage->get_mod()' ); } $use_post = apply_filters( 'wpsso_use_post', false ); $mod = $this->p->page->get_mod( $use_post ); if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'mod', $mod ); $this->p->debug->log( 'home url = ' . get_option( 'home' ) ); $this->p->debug->log( 'locale current = ' . SucomUtilWP::get_locale() ); $this->p->debug->log( 'locale default = ' . SucomUtilWP::get_locale( 'default' ) ); $this->p->debug->log( 'locale mod = ' . SucomUtilWP::get_locale( $mod ) ); $this->p->util->log_is_functions(); } echo $this->get_head_html( $use_post, $mod, $read_cache = true ); } public function get_head_html( $use_post = false, $mod = false, $read_cache = true ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $mtime_start = microtime( $get_float = true ); $indent_num = 0; /* * The $mod array argument is preferred but not required. */ if ( ! is_array( $mod ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'optional call to WpssoPage->get_mod()' ); } $mod = $this->p->page->get_mod( $use_post ); } $html = "\n\n"; if ( apply_filters( 'wpsso_head_disable', false, $mod ) ) { // Not disabled by default. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'head structured data disabled' ); } $head_tags = $this->get_head_array_disabled( $use_post, $mod ); } else $head_tags = $this->get_head_array( $use_post, $mod, $read_cache ); if ( is_array( $head_tags ) ) { // Just in case. foreach ( $head_tags as $mt ) { if ( ! empty( $mt[ 0 ] ) ) { if ( $indent_num && 0 === strpos( $mt[ 0 ], 'get_mt_data( 'added', $mtime_total ) . "\n"; return $html; } /* * The cache is cleared by WpssoAbstractWpMeta->clear_mod_cache(). * * The clear_head_array() method is called with $mod = false and a $canonical_url value for post date archive pages. */ public function clear_head_array( $mod = false, $canonical_url = null ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } if ( empty( $mod ) && empty( $canonical_url ) ) { return 0; } elseif ( null === $canonical_url ) { /* * Canonical URL for SucomUtil::get_mod_salt(). */ $canonical_url = $this->p->util->get_canonical_url( $mod ); } /* * Setup variables for transient cache. * * 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(). */ $pretty_salt = $this->p->util->is_json_pretty() ? '_is_pretty' : ''; $cache_md5_pre = 'wpsso_h_'; // Transient prefix for head markup. $cache_salt = __CLASS__ . '::head_array(' . SucomUtil::get_mod_salt( $mod, $canonical_url ) . $pretty_salt . ')'; $cache_id = $cache_md5_pre . md5( $cache_salt ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'canonical url = ' . $canonical_url ); $this->p->debug->log( 'cache salt = ' . $cache_salt ); $this->p->debug->log( 'cache id = ' . $cache_id ); } delete_transient( $cache_id ); return; } /* * Called when the 'wpsso_head_disable' filter returns true. */ public function get_head_array_disabled( $use_post = false, $mod = false ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } /* * Generator meta tags. */ $mt_gen = array( 'generator' => $this->p->check->get_ext_gen_list() ); return array_merge( array( array( $this->get_mt_data( 'begin' ) ) ), array( array( $this->get_mt_data( 'disabled' ) ) ), $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_gen, $mod ), array( array( $this->get_mt_data( 'end' ) ) ), ); } /* * The cache is cleared by WpssoAbstractWpMeta->clear_mod_cache(). * * $read_cache is false when called by the post/term/user load_meta_page() method. * * See WpssoAbstractWpMeta->get_head_info(). * See WpssoHead->get_head_html(). * See WpssoUtilCache->refresh_mod_head_meta(). */ public function get_head_array( $use_post = false, $mod = false, $read_cache = true ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark( 'building head array' ); // Begin timer. } /* * The $mod array argument is preferred but not required. */ if ( ! is_array( $mod ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'optional call to WpssoPage->get_mod()' ); } $mod = $this->p->page->get_mod( $use_post ); } /* * Canonical URL for SucomUtil::get_mod_salt(). */ $canonical_url = $this->p->util->get_canonical_url( $mod ); /* * Disable head and content cache if the request URL contains one or more unknown query arguments. */ if ( SucomUtilWP::doing_frontend() ) { $request_url = SucomUtil::get_url( $remove_ignored_args = true ); // Uses a local cache. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'request url = ' . $request_url ); } if ( $request_url !== $canonical_url && false !== strpos( $request_url, '?' ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'unknown query arguments detected in request url' ); } /* * WooCommerce product attributes do not have their own webpages - product attribute query * strings are used to pre-fill product selections on the front-end. The * WpssoIntegEcomWooCommerce->filter_request_url_query_cache_disable() method removes all * product attributes from the request URL, and if the $request_url and $canonical_url * values match, the filter will return false (ie. do not disable the cache). */ if ( apply_filters( 'wpsso_request_url_query_cache_disable', true, $request_url, $canonical_url, $mod ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'head and content cache disabled for query arguments' ); } $this->p->util->add_plugin_filters( $this, array( 'cache_expire_head_markup' => '__return_zero', // Used by WpssoHead->get_head_array(). 'cache_expire_the_content' => '__return_zero', // Used by WpssoPage->get_the_content(). ) ); } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'head and content cache allowed for query arguments' ); } } } } /* * Setup variables for transient cache. * * Note that WpssoUtil->get_cache_exp_secs() will return 0 for some pre-defined conditions in the $mod * array (ie. 404, attachment, date, and search). * * 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(). */ $pretty_salt = $this->p->util->is_json_pretty() ? '_is_pretty' : ''; $cache_md5_pre = 'wpsso_h_'; // Transient prefix for head markup. $cache_exp_secs = $this->p->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient', $mod ); $cache_salt = __CLASS__ . '::head_array(' . SucomUtil::get_mod_salt( $mod, $canonical_url ) . $pretty_salt . ')'; $cache_id = $cache_md5_pre . md5( $cache_salt ); $cache_index = $this->get_head_cache_index( $mod ); // Includes the page number. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'canonical url = ' . $canonical_url ); $this->p->debug->log( 'cache expire = ' . $cache_exp_secs ); $this->p->debug->log( 'cache salt = ' . $cache_salt ); $this->p->debug->log( 'cache id = ' . $cache_id ); $this->p->debug->log( 'cache index = ' . $cache_index ); $this->p->debug->log( 'read cache is ' . SucomUtil::get_bool_string( $read_cache ) ); } $cache_array = get_transient( $cache_id ); if ( is_array( $cache_array ) ) { if ( isset( $cache_array[ $cache_index ] ) ) { if ( is_array( $cache_array[ $cache_index ] ) ) { /* * $cache_exp_secs can be 0 if URL query arguments are present or caching has been * disabled for debugging. */ if ( $cache_exp_secs > 0 && $read_cache ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache index found in transient cache' ); $this->p->debug->mark( 'building head array' ); // End timer. } return $cache_array[ $cache_index ]; // Stop here. } unset( $cache_array[ $cache_index ] ); } } } else $cache_array = array(); // Initialize a new transient cache array. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache index not in transient cache' ); } /* * Set a general reference value for admin notices. */ $this->p->util->maybe_set_ref( $canonical_url, $mod ); /* * Define the author_id (if one is available). */ $author_id = WpssoUser::get_author_id( $mod ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'author_id = ' . ( false === $author_id ? 'false' : $author_id ) ); } /* * Open Graph - define first to pass the mt_og array to other methods. */ $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding open graph meta tags', 'wpsso' ) ); $mt_og = $this->p->og->get_array( $mod ); $this->p->util->maybe_unset_ref( $canonical_url ); /* * X (Twitter) Cards. */ $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding twitter card meta tags', 'wpsso' ) ); $mt_tc = $this->p->tc->get_array( $mod, $mt_og, $author_id ); $this->p->util->maybe_unset_ref( $canonical_url ); /* * Name / SEO meta tags. */ $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding meta name meta tags', 'wpsso' ) ); $mt_name = $this->p->meta_name->get_array( $mod, $mt_og, $author_id ); $this->p->util->maybe_unset_ref( $canonical_url ); /* * Link relation tags. */ $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding link relation tags', 'wpsso' ) ); $link_rel = $this->p->link_rel->get_array( $mod, $mt_og, $author_id ); $this->p->util->maybe_unset_ref( $canonical_url ); /* * Schema json scripts. */ if ( empty( $this->p->avail[ 'p' ][ 'schema' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'schema markup is disabled' ); } $schema_scripts = array(); } else { $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding schema json-ld markup', 'wpsso' ) ); $schema_scripts = $this->p->schema->get_array( $mod, $mt_og ); $this->p->util->maybe_unset_ref( $canonical_url ); } /* * Unset mis-matched og_type meta tags. */ $mt_og = $this->p->og->sanitize_mt_array( $mt_og ); /* * Generator meta tags. */ $mt_gen = array( 'generator' => $this->p->check->get_ext_gen_list() ); /* * Combine and return all meta tags. */ $cache_array[ $cache_index ] = array_merge( array( array( $this->get_mt_data( 'begin' ) ) ), $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_gen, $mod ), $this->get_mt_array( $tag = 'link', $type = 'rel', $link_rel, $mod ), $this->get_mt_array( $tag = 'meta', $type = 'property', $mt_og, $mod ), $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_tc, $mod ), $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_name, $mod ), // SEO description is last. $schema_scripts, array( array( $this->get_mt_data( 'end' ) ), array( $this->get_mt_data( 'cached', $cache_exp_secs ) ) ), ); /* * Unset variables that are no longer needed. */ unset( $mt_gen, $link_rel, $mt_og, $mt_tc, $mt_name, $schema_scripts ); /* * Update the cached array and maintain the existing transient expiration time. */ if ( $cache_exp_secs > 0 ) { $expires_in_secs = SucomUtilWP::update_transient_array( $cache_id, $cache_array, $cache_exp_secs ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'head array saved to transient cache (expires in ' . $expires_in_secs . ' secs)' ); } } /* * Unset the general reference value for admin notices. */ $this->p->util->maybe_unset_ref( $canonical_url ); if ( $this->p->debug->enabled ) { $this->p->debug->mark( 'building head array' ); // End timer. } return $cache_array[ $cache_index ]; } /* * A cache index string based on current locale or $mod array. * * $mixed = 'default' | 'current' | post ID | $mod array * * See WpssoHead->get_head_array(). * See WpssoIntegEcomWooCommerce->filter_head_cache_index(). */ public function get_head_cache_index( $mixed = 'current', $sep = '_' ) { $cache_index = ''; if ( is_array( $mixed ) ) { if ( ! empty( $mixed[ 'paged' ] ) && $mixed[ 'paged' ] > 1 ) { // Greater than 1. $cache_index .= $sep . 'paged:' . $mixed[ 'paged' ]; } if ( ! empty( $mixed[ 'query_vars' ][ 'cpage' ] ) && $mixed[ 'query_vars' ][ 'cpage' ] > 1 ) { // Greater than 1. $cache_index .= $sep . 'cpage:' . $mixed[ 'query_vars' ][ 'cpage' ]; } if ( ! empty( $mixed[ 'query_vars' ][ 'order' ] ) ) { // Sort order. $cache_index .= $sep . 'order:' . $mixed[ 'query_vars' ][ 'order' ]; } } $cache_index .= $sep . 'locale:' . SucomUtilWP::get_locale( $mixed ); if ( SucomUtilWP::is_amp() ) $cache_index .= $sep . 'is_amp'; // Returns null, true, or false. if ( function_exists( 'is_embed' ) && is_embed() ) $cache_index .= $sep . 'is_embed'; // Embedded pages are "noindex" by default. $cache_index .= $sep . 'status:' . $this->p->check->get_ext_status( 'wpsso' ); $cache_index = apply_filters( 'wpsso_head_cache_index', ltrim( $cache_index, $sep ), $mixed ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'head cache index = ' . $cache_index ); } return $cache_index; } /* * Extract certain key fields for display and sanity checks. * * Save meta tag values for later sorting in list tables. * * Called by WpssoAbstractWpMeta->get_head_info(). * Called by WpssoPost->load_meta_page(). * Called by WpssoPost->ajax_get_metabox_sso(). * Called by WpssoTerm->load_meta_page(). * Called by WpssoUser->load_meta_page(). * Called by WpssoUtilCache->refresh_mod_head_meta(). */ public function extract_head_info( array $head_tags, $mod = false ) { $head_info = array(); foreach ( $head_tags as $mt ) { if ( ! isset( $mt[ 2 ] ) || ! isset( $mt[ 3 ] ) ) { continue; } $mt_match = $mt[ 2 ] . '-' . $mt[ 3 ]; switch ( $mt_match ) { case 'property-og:description': case 'property-og:locale': case 'property-og:title': case 'property-og:type': case 'property-og:url': case 'property-article:author:name': case ( strpos( $mt_match, 'name-schema:' ) === 0 ? true : false ): case ( strpos( $mt_match, 'name-twitter:' ) === 0 ? true : false ): if ( ! isset( $head_info[ $mt[ 3 ] ] ) ) { // Only save the first meta tag value. $head_info[ $mt[ 3 ] ] = $mt[ 5 ]; } break; case ( preg_match( '/^property-((og|p):(image|video))(:secure_url|:url)?$/', $mt_match, $m ) ? true : false ): if ( ! empty( $mt[ 5 ] ) ) { // Just in case. $has_media[ $m[ 1 ] ] = true; // Optimize media loop. } break; case 'property-og:redirect_url': if ( ! empty( $mt[ 5 ] ) ) { // Just in case. $head_info[ 'is_redirect' ] = '1'; } break; case 'name-robots': if ( ! empty( $mt[ 5 ] ) ) { // Just in case. $directives = $this->p->util->robots->get_content_directives( $mt[ 5 ] ); if ( isset( $directives[ 'noindex' ] ) ) { // Empty string. $head_info[ 'is_noindex' ] = '1'; } } break; } } /* * Save the first image and video information found. * * Assumes array key order defined by SucomUtil::get_mt_image_seed() and SucomUtil::get_mt_video_seed(). */ foreach ( array( 'og:image', 'og:video', 'p:image' ) as $mt_pre ) { if ( empty( $has_media[ $mt_pre ] ) ) { continue; } $is_first = false; foreach ( $head_tags as $mt ) { if ( ! isset( $mt[ 2 ] ) || ! isset( $mt[ 3 ] ) ) { continue; } if ( strpos( $mt[ 3 ], $mt_pre ) !== 0 ) { $is_first = false; /* * If we already found media, then skip to the next media prefix. */ if ( ! empty( $head_info[ $mt_pre ] ) ) { continue 2; } else { continue; // Skip meta tags without matching prefix. } } $mt_match = $mt[ 2 ] . '-' . $mt[ 3 ]; switch ( $mt_match ) { case ( preg_match( '/^property-' . $mt_pre . '(:secure_url|:url)?$/', $mt_match, $m ) ? true : false ): if ( ! empty( $head_info[ $mt_pre ] ) ) { // Only save the media URL once. continue 2; // Get the next meta tag. } if ( ! empty( $mt[ 5 ] ) ) { $head_info[ $mt_pre ] = $mt[ 5 ]; // Save the media URL. $is_first = true; } break; case ( preg_match( '/^property-' . $mt_pre . ':(width|height|cropped|id|title|description)$/', $mt_match, $m ) ? true : false ): if ( true !== $is_first ) { // Only save for first media found. continue 2; // Get the next meta tag. } $head_info[ $mt[ 3 ] ] = $mt[ 5 ]; break; } } } /* * Save meta tag values for later sorting in list tables. * * Not that the column 'meta_key' value must begin with '_wpsso_head_info_'. */ if ( isset( $_GET[ 'replytocom' ] ) || is_embed() || is_404() || is_search() ) { // Nothing to do. } elseif ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) { $sortable_cols = WpssoAbstractWpMeta::get_sortable_columns(); // Uses a local cache. foreach ( $sortable_cols as $col_key => $col_info ) { if ( empty( $col_info[ 'meta_key' ] ) ) { continue; } elseif ( 0 !== strpos( $col_info[ 'meta_key' ], '_wpsso_head_info_' ) ) { continue; } $meta_value = $col_info[ 'def_val' ] ? $col_info[ 'def_val' ] : 'none'; // Default value. if ( isset( $col_info[ 'mt_name' ] ) ) { if ( 'og:image' === $col_info[ 'mt_name' ] ) { // Get the image thumbnail HTML. /* * Example $media_html: * *
*/ if ( $media_html = $mod[ 'obj' ]->get_head_info_thumb_bg_img( $head_info, $mod ) ) { $meta_value = $media_html; } } elseif ( isset( $head_info[ $col_info[ 'mt_name' ] ] ) && '' !== $head_info[ $col_info[ 'mt_name' ] ] ) { $meta_value = $head_info[ $col_info[ 'mt_name' ] ]; } } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'updating meta for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] . ' ' . $col_key . ' = ' . $meta_value ); } $mod[ 'obj' ]->update_sortable_meta( $mod[ 'id' ], $col_key, $meta_value ); } } return $head_info; } private function get_mt_array( $tag, $type, array $mixed, array $mod ) { $mt_array = array(); $this->add_mt_array( $mt_array, $tag, $type, $name = '', $mixed, $cmt = '', $mod, $use_image = true ); return $mt_array; } /* * Called by WpssoHead->get_head_array() with $type = 'begin', 'cached', and 'end'. * Called by WpssoHead->get_head_html() with $type = 'added'. * Called by WpssoPost->check_post_head() with $type = 'preg'. * Called by WpssoSsmFilters->strip_schema_microdata() with $type = 'preg'. */ public function get_mt_data( $type, $args = null ) { switch ( $type ) { case 'added': $total_secs = sprintf( '%f secs', $args ); $human_mem = SucomUtil::format_human_bytes( memory_get_peak_usage(), $dec = 2 ); return '' . "\n"; case 'begin': case 'disabled': case 'end': return '' . "\n"; case 'cached': $home_url = SucomUtilWP::raw_get_home_url(); $home_path = preg_replace( '/^[a-z]+:\/\//i', '', $home_url ); // Remove the protocol prefix. /* * $args = $cache_exp_secs. */ return '' . "\n"; /* * Used by WpssoPost->check_post_head() and WpssoSsmFilters->strip_schema_microdata(). */ case 'preg': /* * Some HTML optimization plugins or services may remove the double-quotes from the name * attribute, along with the trailing space and slash characters, so make these optional in * the regex. */ $preg_prefix = '<(meta[\s\n\r]+name="?wpsso-(begin|end)"?[\s\n\r]+content=")'; $preg_suffix = '("[\s\n\r]*\/?)>'; /* * U = Invert greediness of quantifiers, so they are NOT greedy by default, but become greedy if followed by ?. * u = Modifier to handle UTF-8 in subject strings. * m = The "^" and "$" constructs match newlines and the complete subject string. * s = A dot metacharacter in the pattern matches all characters, including newlines. */ return '/' . $preg_prefix . WPSSO_DATA_ID . ' begin' . $preg_suffix . '.*' . $preg_prefix . WPSSO_DATA_ID . ' end' . $preg_suffix . '/Uums'; default: return ''; } } /* * Loops through the arrays and calls self->add_mt_singles() for each. * * The $name argument is required for numeric arrays. * * $mixed can be a null, boolean, a string, numeric, a numeric array, or an associative array. */ private function add_mt_array( array &$mt_array, $tag, $type, $name, $mixed, $cmt = '', $mod = false, $use_image = true ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $mixed ); } if ( is_array( $mixed ) ) { if ( SucomUtil::is_assoc( $mixed ) ) { if ( isset( $mixed[ 'og:video:type' ] ) ) { if ( empty( $mixed[ 'og:video:has_image' ] ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'ignoring video preview images' ); } $use_image = false; } } foreach ( $mixed as $key => $value ) { $log_prefix = $tag . ' ' . $type . ' ' . $key; if ( ! $use_image && 0 === strpos( $key, 'og:image' ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' skipped: use image is false' ); } } elseif ( is_array( $value ) ) { $opt_key = strtolower( 'add_' . $tag . '_' . $type . '_' . $key ); if ( ! empty( $this->p->options[ $opt_key ] ) || 0 === strpos( $key, 'wpsso-' ) ) { $this->add_mt_array( $mt_array, $tag, $type, $key, $value, $cmt, $mod, $use_image ); } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' array skipped: option is disabled' ); } } else $this->add_mt_singles( $mt_array, $tag, $type, $key, $value, $cmt, $mod ); } } else { foreach ( $mixed as $num => $value ) { $cmt_num = ltrim( $cmt . ':' . $name . ':' . ( $num + 1 ), ':' ); if ( is_array( $value ) ) { $this->add_mt_array( $mt_array, $tag, $type, $name, $value, $cmt_num, $mod, $use_image ); } else $this->add_mt_singles( $mt_array, $tag, $type, $name, $value, $cmt_num, $mod ); } } } else $this->add_mt_singles( $mt_array, $tag, $type, $name, $mixed, $cmt, $mod ); return $mt_array; } public function add_mt_singles( &$mt_array, $tag, $type, $name, $value, $cmt = '', $mod = false ) { static $last_secure_url = null; static $last_url = null; static $charset = null; if ( null === $charset ) { $charset = get_bloginfo( $show = 'charset', $filter = 'raw' ); } /* * Check for known exceptions for the "property" $type. */ if ( 'meta' === $tag ) { if ( 'property' === $type ) { /* * Double-check the name to make sure its an open graph meta tag. */ switch ( $name ) { /* * These names are not open graph meta tag names. */ case ( strpos( $name, 'twitter:' ) === 0 ? true : false ): case ( strpos( $name, 'schema:' ) === 0 ? true : false ): // Internal meta tags. case ( strpos( $name, ':' ) === false ? true : false ): // No colon in $name. $type = 'name'; break; } } elseif ( 'itemprop' === $type ) { /* * If an "itemprop" contains a url, then make sure it's a "link". */ if ( $tag !== 'link' && is_string( $value ) && false !== filter_var( $value, FILTER_VALIDATE_URL ) ) { $tag = 'link'; } } } /* * Check for "link rel href" and "link itemprop href" - all other meta tags use a 'content' attribute name. */ $attr = 'link' === $tag ? 'href' : 'content'; $log_prefix = $tag . ' ' . $type . ' ' . $name; if ( null === $value ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' value is null (skipped)' ); } return $mt_array; } elseif ( is_array( $value ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' value is an array (skipped)' ); } return $mt_array; } elseif ( is_object( $value ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' value is an object (skipped)' ); } return $mt_array; } $singles = array(); switch ( $name ) { case 'og:image:secure_url': case 'og:video:secure_url': /* * The secure url meta tag should always come first, but just in case it doesn't, make sure * the url value has not already been added. */ if ( $last_url !== $value ) { // Just in case. if ( $value ) { $name_secure_url = $name; $name_url = str_replace( ':secure_url', ':url', $name ); $name_no_url = str_replace( ':secure_url', '', $name ); if ( SucomUtil::is_https( $value ) ) { $singles[] = array( '', $tag, $type, $name_secure_url, $attr, $value, $cmt ); } $singles[] = array( '', $tag, $type, $name_url, $attr, $value, $cmt ); $singles[] = array( '', $tag, $type, $name_no_url, $attr, $value, $cmt ); $last_secure_url = $value; if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'singles', $singles ); } } } $last_url = null; break; case 'og:image:url': case 'og:video:url': /* * The previous switch block would have set all three meta tags already, so only proceed if * the last secure url (empty or not) is not equal to the current url value (empty or not). */ if ( $last_secure_url !== $value ) { // Just in case. if ( $value ) { $name_secure_url = str_replace( ':url', ':secure_url', $name ); $name_url = $name; $name_no_url = str_replace( ':url', '', $name ); if ( SucomUtil::is_https( $value ) ) { $singles[] = array( '', $tag, $type, $name_secure_url, $attr, $value, $cmt ); } $singles[] = array( '', $tag, $type, $name_url, $attr, $value, $cmt ); $singles[] = array( '', $tag, $type, $name_no_url, $attr, $value, $cmt ); $last_url = $value; if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'singles', $singles ); } } } $last_secure_url = null; break; case 'og:image': case 'og:video': /* * The previous two switch blocks would have set all three meta tags aready, so only * proceed if we have a value, and it does not match the last secure url (empty or not) or * the last insecure url (empty or not). */ if ( empty( $value ) || $value === $last_secure_url || $value === $last_url ) { break; } // No break. default: $singles[] = array( '', $tag, $type, $name, $attr, $value, $cmt ); break; } /* * $parts = array( $html, $tag, $type, $name, $attr, $value, $cmt ); */ foreach ( $singles as $num => $parts ) { if ( ! array_key_exists( 6, $parts ) ) { // Just in case - check for missing $value element. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'parts array is incomplete (skipped)' ); $this->p->debug->log_arr( 'parts', $parts ); } continue; } $log_prefix = $parts[ 1 ] . ' ' . $parts[ 2 ] . ' ' . $parts[ 3 ]; if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' = "' . $parts[ 5 ] . '"' ); } if ( '' === $parts[ 5 ] || null === $parts[ 5 ] ) { // Allow for 0. if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' skipped: value is empty' ); } $singles[ $num ][ 5 ] = ''; // Avoid null values in REST API output. continue; } elseif ( $parts[ 5 ] === WPSSO_UNDEF || $parts[ 5 ] === (string) WPSSO_UNDEF ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $log_prefix . ' skipped: value is ' . WPSSO_UNDEF ); } continue; } /* * Encode and escape all values, regardless if the head tag is enabled or not. If the head tag is * enabled, HTML will be created and saved in $parts[ 0 ]. */ if ( 'itemprop' === $parts[ 2 ] && 0 !== strpos( $parts[ 3 ], '.' ) ) { $match_name = preg_replace( '/^.*\./', '', $parts[ 3 ] ); } else $match_name = $parts[ 3 ]; /* * Boolean values are converted to their string equivalent. */ if ( is_bool( $parts[ 5 ] ) ) { $parts[ 5 ] = $parts[ 5 ] ? 'true' : 'false'; } switch ( $match_name ) { /* * Description values that may include emoji. */ case 'og:title': case 'og:description': case 'og:image:alt': case 'twitter:title': case 'twitter:description': case 'twitter:image:alt': case 'description': case 'name': $parts[ 5 ] = SucomUtil::encode_html_emoji( $parts[ 5 ] ); // Does not double-encode. break; /* * URL values that must be URL encoded. */ case 'og:secure_url': case 'og:url': case 'og:image:secure_url': case 'og:image:url': case 'og:image': case 'og:video:embed_url': case 'og:video:secure_url': case 'og:video:url': case 'og:video': case 'place:business:menu_url': case 'place:business:order_url': case 'twitter:image': case 'twitter:player': case 'canonical': case 'shortlink': case 'image': case 'hasmenu': // Food establishment menu url. case 'url': $parts[ 5 ] = SucomUtil::esc_url_encode( $parts[ 5 ] ); if ( $parts[ 2 ] === 'itemprop' ) { // An itemprop URL must be a 'link'. $parts[ 1 ] = 'link'; $parts[ 4 ] = 'href'; } break; /* * Allow for mobile app / non-standard protocols. */ case 'al:android:url': case 'al:ios:url': case 'al:web:url': case 'twitter:app:url:iphone': case 'twitter:app:url:ipad': case 'twitter:app:url:googleplay': $parts[ 5 ] = SucomUtil::esc_url_encode( $parts[ 5 ], $esc_url = false ); break; /* * Encode html entities for everything else. */ default: $parts[ 5 ] = htmlentities( $parts[ 5 ], ENT_QUOTES, $charset, $double_encode = false ); break; } /* * Convert mixed case itemprop names to lower case. */ $opt_key = strtolower( 'add_' . $parts[ 1 ] . '_' . $parts[ 2 ] . '_' . $parts[ 3 ] ); if ( ! empty( $this->p->options[ $opt_key ] ) || 0 === strpos( $match_name, 'wpsso-' ) ) { $parts_prefix = empty( $parts[ 6 ] ) ? '' : ''; $parts[ 0 ] = $parts_prefix . '<' . $parts[ 1 ] . ' ' . $parts[ 2 ] . '="' . $match_name . '" ' . $parts[ 4 ] . '="' . $parts[ 5 ] . '"/>' . "\n"; } elseif ( $this->p->debug->enabled ) { if ( isset( $this->p->options[ $opt_key ] ) ) { $this->p->debug->log( $log_prefix . ' skipped: option is disabled' ); } else $this->p->debug->log( $log_prefix . ' skipped: internal meta tag' ); } $mt_array[] = $parts; // Save the HTML and encoded value. } unset( $singles, $num, $parts ); return $mt_array; } } }