PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / trunk
WPSSO Core – Complete Schema Markup and Meta Tags vtrunk
22.6.1 22.6.0 22.5.3 22.5.2 22.5.1 22.4.0 22.3.0 22.2.1 22.2.0 22.1.3 22.1.2 22.1.1 22.1.0 22.0.0 21.13.3 21.13.2 trunk 21.13.1
wpsso / lib / head.php

head.php in WPSSO Core – Complete Schema Markup and Meta Tags trunk, at lib/head.php

1,327 lines 35.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * License: GPLv3
4 * License URI: https://www.gnu.org/licenses/gpl.txt
5 * Copyright 2012-2026 Jean-Sebastien Morisset (https://wpsso.com/)
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9
10 die( 'These aren\'t the droids you\'re looking for.' );
11 }
12
13 if ( ! defined( 'WPSSO_PLUGINDIR' ) ) {
14
15 die( 'Do. Or do not. There is no try.' );
16 }
17
18 if ( ! class_exists( 'WpssoHead' ) ) {
19
20 class WpssoHead {
21
22 private $p; // Wpsso class object.
23
24 public function __construct( &$plugin ) {
25
26 $this->p =& $plugin;
27
28 if ( $this->p->debug->enabled ) {
29
30 $this->p->debug->mark();
31 }
32
33 /*
34 * If any custom modifications are required to the WP_Query 'query_vars', they should be done before the
35 * 'wp_head' action is triggered. The WpssoHead->show_head() method calls WpssoPage->get_mod() to determine
36 * the current WordPress object (comment, post, term, or user), if any, and saves the 'query_vars' value
37 * for WordPress archive queries.
38 */
39 add_action( 'wp_head', array( $this, 'show_head' ), WPSSO_HEAD_PRIORITY );
40
41 /*
42 * If an AMP plugin is active, hook 'amp_post_template_head' to add head markup for AMP pages.
43 */
44 if ( ! empty( $this->p->avail[ 'amp' ][ 'any' ] ) ) {
45
46 add_action( 'amp_post_template_head', array( $this, 'show_head' ), WPSSO_HEAD_PRIORITY );
47
48 if ( $this->p->avail[ 'p' ][ 'schema' ] ) {
49
50 if ( $this->p->debug->enabled ) {
51
52 $this->p->debug->log( 'disabling amp_post_template_metadata' );
53 }
54
55 add_filter( 'amp_post_template_metadata', '__return_empty_array', PHP_INT_MAX );
56 }
57 }
58
59 /*
60 * Maybe do a 301 redirect.
61 */
62 add_action( 'template_redirect', array( $this, 'maybe_redirect_url' ), PHP_INT_MIN );
63 }
64
65 public function maybe_redirect_url() {
66
67 if ( $this->p->debug->enabled ) {
68
69 $this->p->debug->mark();
70 }
71
72 if ( is_admin() || is_preview() || is_customize_preview() ) {
73
74 if ( $this->p->debug->enabled ) {
75
76 $this->p->debug->log( 'exiting early: is admin, preview, or customize preview' );
77 }
78
79 return;
80
81 /*
82 * WpssoUtil->is_redirect_disabled() returns true if:
83 *
84 * - An SEO plugin is active.
85 * - The 'wpsso_redirect_disabled' filter returns true.
86 */
87 } elseif ( $this->p->util->is_redirect_disabled() ) {
88
89 if ( $this->p->debug->enabled ) {
90
91 $this->p->debug->log( 'exiting early: redirect is disabled' );
92 }
93
94 return;
95 }
96
97 $use_post = apply_filters( 'wpsso_use_post', false );
98
99 $url = $this->p->util->get_redirect_url( $use_post );
100
101 if ( $url ) {
102
103 do_action( 'wpsso_before_redirect', $url );
104
105 $redirect_code = absint( apply_filters( 'wpsso_redirect_status_code', 301 ) );
106
107 if ( wp_safe_redirect( $url, $redirect_code ) ) {
108
109 exit;
110 }
111 }
112 }
113
114 public function add_vary_user_agent_header( $headers ) {
115
116 $headers [ 'Vary' ] = 'User-Agent';
117
118 return $headers;
119 }
120
121 /*
122 * Called by 'wp_head' and 'amp_post_template_head' actions.
123 *
124 * If any custom modifications are required to the WP_Query 'query_vars', they should be done before the 'wp_head'
125 * action is triggered. The WpssoHead->show_head() method calls WpssoPage->get_mod() to determine the current
126 * WordPress object (comment, post, term, or user), if any, and saves the 'query_vars' value for WordPress archive
127 * queries.
128 */
129 public function show_head() {
130
131 if ( $this->p->debug->enabled ) {
132
133 $this->p->debug->mark();
134 }
135
136 if ( is_admin() || is_preview() || is_customize_preview() ) {
137
138 if ( $this->p->debug->enabled ) {
139
140 $this->p->debug->log( 'exiting early: is admin, preview, or customize preview' );
141 }
142
143 return;
144 }
145
146 if ( $this->p->debug->enabled ) {
147
148 $this->p->debug->log( 'required call to WpssoPage->get_mod()' );
149 }
150
151 $use_post = apply_filters( 'wpsso_use_post', false );
152
153 $mod = $this->p->page->get_mod( $use_post );
154
155 if ( $this->p->debug->enabled ) {
156
157 $this->p->debug->log_arr( 'mod', $mod );
158 $this->p->debug->log( 'home url = ' . get_option( 'home' ) );
159 $this->p->debug->log( 'locale current = ' . SucomUtilWP::get_locale() );
160 $this->p->debug->log( 'locale default = ' . SucomUtilWP::get_locale( 'default' ) );
161 $this->p->debug->log( 'locale mod = ' . SucomUtilWP::get_locale( $mod ) );
162 $this->p->util->log_is_functions();
163 }
164
165 echo $this->get_head_html( $use_post, $mod, $read_cache = true );
166 }
167
168 public function get_head_html( $use_post = false, $mod = false, $read_cache = true ) {
169
170 if ( $this->p->debug->enabled ) {
171
172 $this->p->debug->mark();
173 }
174
175 $mtime_start = microtime( $get_float = true );
176 $indent_num = 0;
177
178 /*
179 * The $mod array argument is preferred but not required.
180 */
181 if ( ! is_array( $mod ) ) {
182
183 if ( $this->p->debug->enabled ) {
184
185 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
186 }
187
188 $mod = $this->p->page->get_mod( $use_post );
189 }
190
191 $html = "\n\n";
192
193 if ( apply_filters( 'wpsso_head_disable', false, $mod ) ) { // Not disabled by default.
194
195 if ( $this->p->debug->enabled ) {
196
197 $this->p->debug->log( 'head structured data disabled' );
198 }
199
200 $head_tags = $this->get_head_array_disabled( $use_post, $mod );
201
202 } else $head_tags = $this->get_head_array( $use_post, $mod, $read_cache );
203
204 if ( is_array( $head_tags ) ) { // Just in case.
205
206 foreach ( $head_tags as $mt ) {
207
208 if ( ! empty( $mt[ 0 ] ) ) {
209
210 if ( $indent_num && 0 === strpos( $mt[ 0 ], '</noscript' ) ) {
211
212 $indent_num = 0;
213 }
214
215 $html .= str_repeat( "\t", (int) $indent_num ) . $mt[0];
216
217 if ( 0 === strpos( $mt[ 0 ], '<noscript' ) ) { // Indent meta tags within a noscript container.
218
219 $indent_num = 1;
220 }
221 }
222 }
223 }
224
225 unset( $head_tags, $mt, $indent_num, $use_post, $mod, $read_cache );
226
227 $mtime_total = microtime( $get_float = true ) - $mtime_start;
228
229 $html .= $this->get_mt_data( 'added', $mtime_total ) . "\n";
230
231 return $html;
232 }
233
234 /*
235 * The cache is cleared by WpssoAbstractWpMeta->clear_mod_cache().
236 *
237 * The clear_head_array() method is called with $mod = false and a $canonical_url value for post date archive pages.
238 */
239 public function clear_head_array( $mod = false, $canonical_url = null ) {
240
241 if ( $this->p->debug->enabled ) {
242
243 $this->p->debug->mark();
244 }
245
246 if ( empty( $mod ) && empty( $canonical_url ) ) {
247
248 return 0;
249
250 } elseif ( null === $canonical_url ) {
251
252 /*
253 * Canonical URL for SucomUtil::get_mod_salt().
254 */
255 $canonical_url = $this->p->util->get_canonical_url( $mod );
256 }
257
258 /*
259 * Setup variables for transient cache.
260 *
261 * Note that the sort order, page number, locale, amp and embed checks are provided by
262 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
263 */
264 $pretty_salt = $this->p->util->is_json_pretty() ? '_is_pretty' : '';
265 $cache_md5_pre = 'wpsso_h_'; // Transient prefix for head markup.
266 $cache_salt = __CLASS__ . '::head_array(' . SucomUtil::get_mod_salt( $mod, $canonical_url ) . $pretty_salt . ')';
267 $cache_id = $cache_md5_pre . md5( $cache_salt );
268
269 if ( $this->p->debug->enabled ) {
270
271 $this->p->debug->log( 'canonical url = ' . $canonical_url );
272 $this->p->debug->log( 'cache salt = ' . $cache_salt );
273 $this->p->debug->log( 'cache id = ' . $cache_id );
274 }
275
276 delete_transient( $cache_id );
277
278 return;
279 }
280
281 /*
282 * Called when the 'wpsso_head_disable' filter returns true.
283 */
284 public function get_head_array_disabled( $use_post = false, $mod = false ) {
285
286 if ( $this->p->debug->enabled ) {
287
288 $this->p->debug->mark();
289 }
290
291 /*
292 * Generator meta tags.
293 */
294 $mt_gen = array( 'generator' => $this->p->check->get_ext_gen_list() );
295
296 return array_merge(
297 array( array( $this->get_mt_data( 'begin' ) ) ),
298 array( array( $this->get_mt_data( 'disabled' ) ) ),
299 $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_gen, $mod ),
300 array( array( $this->get_mt_data( 'end' ) ) ),
301 );
302 }
303
304 /*
305 * The cache is cleared by WpssoAbstractWpMeta->clear_mod_cache().
306 *
307 * $read_cache is false when called by the post/term/user load_meta_page() method.
308 *
309 * See WpssoAbstractWpMeta->get_head_info().
310 * See WpssoHead->get_head_html().
311 * See WpssoUtilCache->refresh_mod_head_meta().
312 */
313 public function get_head_array( $use_post = false, $mod = false, $read_cache = true ) {
314
315 if ( $this->p->debug->enabled ) {
316
317 $this->p->debug->mark( 'building head array' ); // Begin timer.
318 }
319
320 /*
321 * The $mod array argument is preferred but not required.
322 */
323 if ( ! is_array( $mod ) ) {
324
325 if ( $this->p->debug->enabled ) {
326
327 $this->p->debug->log( 'optional call to WpssoPage->get_mod()' );
328 }
329
330 $mod = $this->p->page->get_mod( $use_post );
331 }
332
333 /*
334 * Canonical URL for SucomUtil::get_mod_salt().
335 */
336 $canonical_url = $this->p->util->get_canonical_url( $mod );
337
338 /*
339 * Disable head and content cache if the request URL contains one or more unknown query arguments.
340 */
341 if ( SucomUtilWP::doing_frontend() ) {
342
343 $request_url = SucomUtil::get_url( $remove_ignored_args = true ); // Uses a local cache.
344
345 if ( $this->p->debug->enabled ) {
346
347 $this->p->debug->log( 'request url = ' . $request_url );
348 }
349
350 if ( $request_url !== $canonical_url && false !== strpos( $request_url, '?' ) ) {
351
352 if ( $this->p->debug->enabled ) {
353
354 $this->p->debug->log( 'unknown query arguments detected in request url' );
355 }
356
357 /*
358 * WooCommerce product attributes do not have their own webpages - product attribute query
359 * strings are used to pre-fill product selections on the front-end. The
360 * WpssoIntegEcomWooCommerce->filter_request_url_query_cache_disable() method removes all
361 * product attributes from the request URL, and if the $request_url and $canonical_url
362 * values match, the filter will return false (ie. do not disable the cache).
363 */
364 if ( apply_filters( 'wpsso_request_url_query_cache_disable', true, $request_url, $canonical_url, $mod ) ) {
365
366 if ( $this->p->debug->enabled ) {
367
368 $this->p->debug->log( 'head and content cache disabled for query arguments' );
369 }
370
371 $this->p->util->add_plugin_filters( $this, array(
372 'cache_expire_head_markup' => '__return_zero', // Used by WpssoHead->get_head_array().
373 'cache_expire_the_content' => '__return_zero', // Used by WpssoPage->get_the_content().
374 ) );
375
376 } else {
377
378 if ( $this->p->debug->enabled ) {
379
380 $this->p->debug->log( 'head and content cache allowed for query arguments' );
381 }
382 }
383 }
384 }
385
386 /*
387 * Setup variables for transient cache.
388 *
389 * Note that WpssoUtil->get_cache_exp_secs() will return 0 for some pre-defined conditions in the $mod
390 * array (ie. 404, attachment, date, and search).
391 *
392 * Note that the sort order, page number, locale, amp and embed checks are provided by
393 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
394 */
395 $pretty_salt = $this->p->util->is_json_pretty() ? '_is_pretty' : '';
396 $cache_md5_pre = 'wpsso_h_'; // Transient prefix for head markup.
397 $cache_exp_secs = $this->p->util->get_cache_exp_secs( $cache_md5_pre, $cache_type = 'transient', $mod );
398 $cache_salt = __CLASS__ . '::head_array(' . SucomUtil::get_mod_salt( $mod, $canonical_url ) . $pretty_salt . ')';
399 $cache_id = $cache_md5_pre . md5( $cache_salt );
400 $cache_index = $this->get_head_cache_index( $mod ); // Includes the page number.
401
402 if ( $this->p->debug->enabled ) {
403
404 $this->p->debug->log( 'canonical url = ' . $canonical_url );
405 $this->p->debug->log( 'cache expire = ' . $cache_exp_secs );
406 $this->p->debug->log( 'cache salt = ' . $cache_salt );
407 $this->p->debug->log( 'cache id = ' . $cache_id );
408 $this->p->debug->log( 'cache index = ' . $cache_index );
409 $this->p->debug->log( 'read cache is ' . SucomUtil::get_bool_string( $read_cache ) );
410 }
411
412 $cache_array = get_transient( $cache_id );
413
414 if ( is_array( $cache_array ) ) {
415
416 if ( isset( $cache_array[ $cache_index ] ) ) {
417
418 if ( is_array( $cache_array[ $cache_index ] ) ) {
419
420 /*
421 * $cache_exp_secs can be 0 if URL query arguments are present or caching has been
422 * disabled for debugging.
423 */
424 if ( $cache_exp_secs > 0 && $read_cache ) {
425
426 if ( $this->p->debug->enabled ) {
427
428 $this->p->debug->log( 'cache index found in transient cache' );
429
430 $this->p->debug->mark( 'building head array' ); // End timer.
431 }
432
433 return $cache_array[ $cache_index ]; // Stop here.
434 }
435
436 unset( $cache_array[ $cache_index ] );
437 }
438 }
439
440 } else $cache_array = array(); // Initialize a new transient cache array.
441
442 if ( $this->p->debug->enabled ) {
443
444 $this->p->debug->log( 'cache index not in transient cache' );
445 }
446
447 /*
448 * Set a general reference value for admin notices.
449 */
450 $this->p->util->maybe_set_ref( $canonical_url, $mod );
451
452 /*
453 * Define the author_id (if one is available).
454 */
455 $author_id = WpssoUser::get_author_id( $mod );
456
457 if ( $this->p->debug->enabled ) {
458
459 $this->p->debug->log( 'author_id = ' . ( false === $author_id ? 'false' : $author_id ) );
460 }
461
462 /*
463 * Open Graph - define first to pass the mt_og array to other methods.
464 */
465 $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding open graph meta tags', 'wpsso' ) );
466
467 $mt_og = $this->p->og->get_array( $mod );
468
469 $this->p->util->maybe_unset_ref( $canonical_url );
470
471 /*
472 * X (Twitter) Cards.
473 */
474 $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding twitter card meta tags', 'wpsso' ) );
475
476 $mt_tc = $this->p->tc->get_array( $mod, $mt_og, $author_id );
477
478 $this->p->util->maybe_unset_ref( $canonical_url );
479
480 /*
481 * Name / SEO meta tags.
482 */
483 $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding meta name meta tags', 'wpsso' ) );
484
485 $mt_name = $this->p->meta_name->get_array( $mod, $mt_og, $author_id );
486
487 $this->p->util->maybe_unset_ref( $canonical_url );
488
489 /*
490 * Link relation tags.
491 */
492 $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding link relation tags', 'wpsso' ) );
493
494 $link_rel = $this->p->link_rel->get_array( $mod, $mt_og, $author_id );
495
496 $this->p->util->maybe_unset_ref( $canonical_url );
497
498 /*
499 * Schema json scripts.
500 */
501 if ( empty( $this->p->avail[ 'p' ][ 'schema' ] ) ) {
502
503 if ( $this->p->debug->enabled ) {
504
505 $this->p->debug->log( 'schema markup is disabled' );
506 }
507
508 $schema_scripts = array();
509
510 } else {
511
512 $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'adding schema json-ld markup', 'wpsso' ) );
513
514 $schema_scripts = $this->p->schema->get_array( $mod, $mt_og );
515
516 $this->p->util->maybe_unset_ref( $canonical_url );
517 }
518
519 /*
520 * Unset mis-matched og_type meta tags.
521 */
522 $mt_og = $this->p->og->sanitize_mt_array( $mt_og );
523
524 /*
525 * Generator meta tags.
526 */
527 $mt_gen = array( 'generator' => $this->p->check->get_ext_gen_list() );
528
529 /*
530 * Combine and return all meta tags.
531 */
532 $cache_array[ $cache_index ] = array_merge(
533 array( array( $this->get_mt_data( 'begin' ) ) ),
534 $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_gen, $mod ),
535 $this->get_mt_array( $tag = 'link', $type = 'rel', $link_rel, $mod ),
536 $this->get_mt_array( $tag = 'meta', $type = 'property', $mt_og, $mod ),
537 $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_tc, $mod ),
538 $this->get_mt_array( $tag = 'meta', $type = 'name', $mt_name, $mod ), // SEO description is last.
539 $schema_scripts,
540 array( array( $this->get_mt_data( 'end' ) ), array( $this->get_mt_data( 'cached', $cache_exp_secs ) ) ),
541 );
542
543 /*
544 * Unset variables that are no longer needed.
545 */
546 unset( $mt_gen, $link_rel, $mt_og, $mt_tc, $mt_name, $schema_scripts );
547
548 /*
549 * Update the cached array and maintain the existing transient expiration time.
550 */
551 if ( $cache_exp_secs > 0 ) {
552
553 $expires_in_secs = SucomUtilWP::update_transient_array( $cache_id, $cache_array, $cache_exp_secs );
554
555 if ( $this->p->debug->enabled ) {
556
557 $this->p->debug->log( 'head array saved to transient cache (expires in ' . $expires_in_secs . ' secs)' );
558 }
559 }
560
561 /*
562 * Unset the general reference value for admin notices.
563 */
564 $this->p->util->maybe_unset_ref( $canonical_url );
565
566 if ( $this->p->debug->enabled ) {
567
568 $this->p->debug->mark( 'building head array' ); // End timer.
569 }
570
571 return $cache_array[ $cache_index ];
572 }
573
574 /*
575 * A cache index string based on current locale or $mod array.
576 *
577 * $mixed = 'default' | 'current' | post ID | $mod array
578 *
579 * See WpssoHead->get_head_array().
580 * See WpssoIntegEcomWooCommerce->filter_head_cache_index().
581 */
582 public function get_head_cache_index( $mixed = 'current', $sep = '_' ) {
583
584 $cache_index = '';
585
586 if ( is_array( $mixed ) ) {
587
588 if ( ! empty( $mixed[ 'paged' ] ) && $mixed[ 'paged' ] > 1 ) { // Greater than 1.
589
590 $cache_index .= $sep . 'paged:' . $mixed[ 'paged' ];
591 }
592
593 if ( ! empty( $mixed[ 'query_vars' ][ 'cpage' ] ) && $mixed[ 'query_vars' ][ 'cpage' ] > 1 ) { // Greater than 1.
594
595 $cache_index .= $sep . 'cpage:' . $mixed[ 'query_vars' ][ 'cpage' ];
596 }
597
598 if ( ! empty( $mixed[ 'query_vars' ][ 'order' ] ) ) { // Sort order.
599
600 $cache_index .= $sep . 'order:' . $mixed[ 'query_vars' ][ 'order' ];
601 }
602 }
603
604 $cache_index .= $sep . 'locale:' . SucomUtilWP::get_locale( $mixed );
605
606 if ( SucomUtilWP::is_amp() ) $cache_index .= $sep . 'is_amp'; // Returns null, true, or false.
607
608 if ( function_exists( 'is_embed' ) && is_embed() ) $cache_index .= $sep . 'is_embed'; // Embedded pages are "noindex" by default.
609
610 $cache_index .= $sep . 'status:' . $this->p->check->get_ext_status( 'wpsso' );
611
612 $cache_index = apply_filters( 'wpsso_head_cache_index', ltrim( $cache_index, $sep ), $mixed );
613
614 if ( $this->p->debug->enabled ) {
615
616 $this->p->debug->log( 'head cache index = ' . $cache_index );
617 }
618
619 return $cache_index;
620 }
621
622 /*
623 * Extract certain key fields for display and sanity checks.
624 *
625 * Save meta tag values for later sorting in list tables.
626 *
627 * Called by WpssoAbstractWpMeta->get_head_info().
628 * Called by WpssoPost->load_meta_page().
629 * Called by WpssoPost->ajax_get_metabox_sso().
630 * Called by WpssoTerm->load_meta_page().
631 * Called by WpssoUser->load_meta_page().
632 * Called by WpssoUtilCache->refresh_mod_head_meta().
633 */
634 public function extract_head_info( array $head_tags, $mod = false ) {
635
636 $head_info = array();
637
638 foreach ( $head_tags as $mt ) {
639
640 if ( ! isset( $mt[ 2 ] ) || ! isset( $mt[ 3 ] ) ) {
641
642 continue;
643 }
644
645 $mt_match = $mt[ 2 ] . '-' . $mt[ 3 ];
646
647 switch ( $mt_match ) {
648
649 case 'property-og:description':
650 case 'property-og:locale':
651 case 'property-og:title':
652 case 'property-og:type':
653 case 'property-og:url':
654 case 'property-article:author:name':
655 case ( strpos( $mt_match, 'name-schema:' ) === 0 ? true : false ):
656 case ( strpos( $mt_match, 'name-twitter:' ) === 0 ? true : false ):
657
658 if ( ! isset( $head_info[ $mt[ 3 ] ] ) ) { // Only save the first meta tag value.
659
660 $head_info[ $mt[ 3 ] ] = $mt[ 5 ];
661 }
662
663 break;
664
665 case ( preg_match( '/^property-((og|p):(image|video))(:secure_url|:url)?$/', $mt_match, $m ) ? true : false ):
666
667 if ( ! empty( $mt[ 5 ] ) ) { // Just in case.
668
669 $has_media[ $m[ 1 ] ] = true; // Optimize media loop.
670 }
671
672 break;
673
674 case 'property-og:redirect_url':
675
676 if ( ! empty( $mt[ 5 ] ) ) { // Just in case.
677
678 $head_info[ 'is_redirect' ] = '1';
679 }
680
681 break;
682
683 case 'name-robots':
684
685 if ( ! empty( $mt[ 5 ] ) ) { // Just in case.
686
687 $directives = $this->p->util->robots->get_content_directives( $mt[ 5 ] );
688
689 if ( isset( $directives[ 'noindex' ] ) ) { // Empty string.
690
691 $head_info[ 'is_noindex' ] = '1';
692 }
693 }
694
695 break;
696 }
697 }
698
699 /*
700 * Save the first image and video information found.
701 *
702 * Assumes array key order defined by SucomUtil::get_mt_image_seed() and SucomUtil::get_mt_video_seed().
703 */
704 foreach ( array( 'og:image', 'og:video', 'p:image' ) as $mt_pre ) {
705
706 if ( empty( $has_media[ $mt_pre ] ) ) {
707
708 continue;
709 }
710
711 $is_first = false;
712
713 foreach ( $head_tags as $mt ) {
714
715 if ( ! isset( $mt[ 2 ] ) || ! isset( $mt[ 3 ] ) ) {
716
717 continue;
718 }
719
720 if ( strpos( $mt[ 3 ], $mt_pre ) !== 0 ) {
721
722 $is_first = false;
723
724 /*
725 * If we already found media, then skip to the next media prefix.
726 */
727 if ( ! empty( $head_info[ $mt_pre ] ) ) {
728
729 continue 2;
730
731 } else {
732
733 continue; // Skip meta tags without matching prefix.
734 }
735 }
736
737 $mt_match = $mt[ 2 ] . '-' . $mt[ 3 ];
738
739 switch ( $mt_match ) {
740
741 case ( preg_match( '/^property-' . $mt_pre . '(:secure_url|:url)?$/', $mt_match, $m ) ? true : false ):
742
743 if ( ! empty( $head_info[ $mt_pre ] ) ) { // Only save the media URL once.
744
745 continue 2; // Get the next meta tag.
746 }
747
748 if ( ! empty( $mt[ 5 ] ) ) {
749
750 $head_info[ $mt_pre ] = $mt[ 5 ]; // Save the media URL.
751
752 $is_first = true;
753 }
754
755 break;
756
757 case ( preg_match( '/^property-' . $mt_pre . ':(width|height|cropped|id|title|description)$/', $mt_match, $m ) ? true : false ):
758
759 if ( true !== $is_first ) { // Only save for first media found.
760
761 continue 2; // Get the next meta tag.
762 }
763
764 $head_info[ $mt[ 3 ] ] = $mt[ 5 ];
765
766 break;
767 }
768 }
769 }
770
771 /*
772 * Save meta tag values for later sorting in list tables.
773 *
774 * Not that the column 'meta_key' value must begin with '_wpsso_head_info_'.
775 */
776 if ( isset( $_GET[ 'replytocom' ] ) || is_embed() || is_404() || is_search() ) {
777
778 // Nothing to do.
779
780 } elseif ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
781
782 $sortable_cols = WpssoAbstractWpMeta::get_sortable_columns(); // Uses a local cache.
783
784 foreach ( $sortable_cols as $col_key => $col_info ) {
785
786 if ( empty( $col_info[ 'meta_key' ] ) ) {
787
788 continue;
789
790 } elseif ( 0 !== strpos( $col_info[ 'meta_key' ], '_wpsso_head_info_' ) ) {
791
792 continue;
793 }
794
795 $meta_value = $col_info[ 'def_val' ] ? $col_info[ 'def_val' ] : 'none'; // Default value.
796
797 if ( isset( $col_info[ 'mt_name' ] ) ) {
798
799 if ( 'og:image' === $col_info[ 'mt_name' ] ) { // Get the image thumbnail HTML.
800
801 /*
802 * Example $media_html:
803 *
804 * <div class="wp-thumb-bg-img" style="background-image:url(https://.../thumbnail.jpg);"></div>
805 */
806 if ( $media_html = $mod[ 'obj' ]->get_head_info_thumb_bg_img( $head_info, $mod ) ) {
807
808 $meta_value = $media_html;
809 }
810
811 } elseif ( isset( $head_info[ $col_info[ 'mt_name' ] ] ) && '' !== $head_info[ $col_info[ 'mt_name' ] ] ) {
812
813 $meta_value = $head_info[ $col_info[ 'mt_name' ] ];
814 }
815 }
816
817 if ( $this->p->debug->enabled ) {
818
819 $this->p->debug->log( 'updating meta for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] . ' ' . $col_key . ' = ' . $meta_value );
820 }
821
822 $mod[ 'obj' ]->update_sortable_meta( $mod[ 'id' ], $col_key, $meta_value );
823 }
824 }
825
826 return $head_info;
827 }
828
829 private function get_mt_array( $tag, $type, array $mixed, array $mod ) {
830
831 $mt_array = array();
832
833 $this->add_mt_array( $mt_array, $tag, $type, $name = '', $mixed, $cmt = '', $mod, $use_image = true );
834
835 return $mt_array;
836 }
837
838 /*
839 * Called by WpssoHead->get_head_array() with $type = 'begin', 'cached', and 'end'.
840 * Called by WpssoHead->get_head_html() with $type = 'added'.
841 * Called by WpssoPost->check_post_head() with $type = 'preg'.
842 * Called by WpssoSsmFilters->strip_schema_microdata() with $type = 'preg'.
843 */
844 public function get_mt_data( $type, $args = null ) {
845
846 switch ( $type ) {
847
848 case 'added':
849
850 $total_secs = sprintf( '%f secs', $args );
851 $human_mem = SucomUtil::format_human_bytes( memory_get_peak_usage(), $dec = 2 );
852
853 return '<meta name="wpsso-' . $type . '" content="' . gmdate( 'c' ) .
854 ' in ' . $total_secs . ' (' . $human_mem . ' peak)"/>' . "\n";
855
856 case 'begin':
857 case 'disabled':
858 case 'end':
859
860 return '<meta name="wpsso-' . $type . '" content="' . WPSSO_DATA_ID . ' ' . $type . '"/>' . "\n";
861
862 case 'cached':
863
864 $home_url = SucomUtilWP::raw_get_home_url();
865 $home_path = preg_replace( '/^[a-z]+:\/\//i', '', $home_url ); // Remove the protocol prefix.
866
867 /*
868 * $args = $cache_exp_secs.
869 */
870 return '<meta name="wpsso-' . $type . '" content="' . ( $args ? gmdate( 'c' ) : 'no cache' ) . ' for ' . $home_path . '"/>' . "\n";
871
872 /*
873 * Used by WpssoPost->check_post_head() and WpssoSsmFilters->strip_schema_microdata().
874 */
875 case 'preg':
876
877 /*
878 * Some HTML optimization plugins or services may remove the double-quotes from the name
879 * attribute, along with the trailing space and slash characters, so make these optional in
880 * the regex.
881 */
882 $preg_prefix = '<(meta[\s\n\r]+name="?wpsso-(begin|end)"?[\s\n\r]+content=")';
883 $preg_suffix = '("[\s\n\r]*\/?)>';
884
885 /*
886 * U = Invert greediness of quantifiers, so they are NOT greedy by default, but become greedy if followed by ?.
887 * u = Modifier to handle UTF-8 in subject strings.
888 * m = The "^" and "$" constructs match newlines and the complete subject string.
889 * s = A dot metacharacter in the pattern matches all characters, including newlines.
890 */
891 return '/' . $preg_prefix . WPSSO_DATA_ID . ' begin' . $preg_suffix . '.*' .
892 $preg_prefix . WPSSO_DATA_ID . ' end' . $preg_suffix . '/Uums';
893
894 default:
895
896 return '';
897 }
898 }
899
900 /*
901 * Loops through the arrays and calls self->add_mt_singles() for each.
902 *
903 * The $name argument is required for numeric arrays.
904 *
905 * $mixed can be a null, boolean, a string, numeric, a numeric array, or an associative array.
906 */
907 private function add_mt_array( array &$mt_array, $tag, $type, $name, $mixed, $cmt = '', $mod = false, $use_image = true ) {
908
909 if ( $this->p->debug->enabled ) {
910
911 $this->p->debug->log( $mixed );
912 }
913
914 if ( is_array( $mixed ) ) {
915
916 if ( SucomUtil::is_assoc( $mixed ) ) {
917
918 if ( isset( $mixed[ 'og:video:type' ] ) ) {
919
920 if ( empty( $mixed[ 'og:video:has_image' ] ) ) {
921
922 if ( $this->p->debug->enabled ) {
923
924 $this->p->debug->log( 'ignoring video preview images' );
925 }
926
927 $use_image = false;
928 }
929 }
930
931 foreach ( $mixed as $key => $value ) {
932
933 $log_prefix = $tag . ' ' . $type . ' ' . $key;
934
935 if ( ! $use_image && 0 === strpos( $key, 'og:image' ) ) {
936
937 if ( $this->p->debug->enabled ) {
938
939 $this->p->debug->log( $log_prefix . ' skipped: use image is false' );
940 }
941
942 } elseif ( is_array( $value ) ) {
943
944 $opt_key = strtolower( 'add_' . $tag . '_' . $type . '_' . $key );
945
946 if ( ! empty( $this->p->options[ $opt_key ] ) || 0 === strpos( $key, 'wpsso-' ) ) {
947
948 $this->add_mt_array( $mt_array, $tag, $type, $key, $value, $cmt, $mod, $use_image );
949
950 } elseif ( $this->p->debug->enabled ) {
951
952 $this->p->debug->log( $log_prefix . ' array skipped: option is disabled' );
953 }
954
955 } else $this->add_mt_singles( $mt_array, $tag, $type, $key, $value, $cmt, $mod );
956 }
957
958 } else {
959
960 foreach ( $mixed as $num => $value ) {
961
962 $cmt_num = ltrim( $cmt . ':' . $name . ':' . ( $num + 1 ), ':' );
963
964 if ( is_array( $value ) ) {
965
966 $this->add_mt_array( $mt_array, $tag, $type, $name, $value, $cmt_num, $mod, $use_image );
967
968 } else $this->add_mt_singles( $mt_array, $tag, $type, $name, $value, $cmt_num, $mod );
969 }
970 }
971
972 } else $this->add_mt_singles( $mt_array, $tag, $type, $name, $mixed, $cmt, $mod );
973
974 return $mt_array;
975 }
976
977 public function add_mt_singles( &$mt_array, $tag, $type, $name, $value, $cmt = '', $mod = false ) {
978
979 static $last_secure_url = null;
980 static $last_url = null;
981 static $charset = null;
982
983 if ( null === $charset ) {
984
985 $charset = get_bloginfo( $show = 'charset', $filter = 'raw' );
986 }
987
988 /*
989 * Check for known exceptions for the "property" $type.
990 */
991 if ( 'meta' === $tag ) {
992
993 if ( 'property' === $type ) {
994
995 /*
996 * Double-check the name to make sure its an open graph meta tag.
997 */
998 switch ( $name ) {
999
1000 /*
1001 * These names are not open graph meta tag names.
1002 */
1003 case ( strpos( $name, 'twitter:' ) === 0 ? true : false ):
1004 case ( strpos( $name, 'schema:' ) === 0 ? true : false ): // Internal meta tags.
1005 case ( strpos( $name, ':' ) === false ? true : false ): // No colon in $name.
1006
1007 $type = 'name';
1008
1009 break;
1010 }
1011
1012 } elseif ( 'itemprop' === $type ) {
1013
1014 /*
1015 * If an "itemprop" contains a url, then make sure it's a "link".
1016 */
1017 if ( $tag !== 'link' && is_string( $value ) && false !== filter_var( $value, FILTER_VALIDATE_URL ) ) {
1018
1019 $tag = 'link';
1020 }
1021 }
1022 }
1023
1024 /*
1025 * Check for "link rel href" and "link itemprop href" - all other meta tags use a 'content' attribute name.
1026 */
1027 $attr = 'link' === $tag ? 'href' : 'content';
1028
1029 $log_prefix = $tag . ' ' . $type . ' ' . $name;
1030
1031 if ( null === $value ) {
1032
1033 if ( $this->p->debug->enabled ) {
1034
1035 $this->p->debug->log( $log_prefix . ' value is null (skipped)' );
1036 }
1037
1038 return $mt_array;
1039
1040 } elseif ( is_array( $value ) ) {
1041
1042 if ( $this->p->debug->enabled ) {
1043
1044 $this->p->debug->log( $log_prefix . ' value is an array (skipped)' );
1045 }
1046
1047 return $mt_array;
1048
1049 } elseif ( is_object( $value ) ) {
1050
1051 if ( $this->p->debug->enabled ) {
1052
1053 $this->p->debug->log( $log_prefix . ' value is an object (skipped)' );
1054 }
1055
1056 return $mt_array;
1057 }
1058
1059 $singles = array();
1060
1061 switch ( $name ) {
1062
1063 case 'og:image:secure_url':
1064 case 'og:video:secure_url':
1065
1066 /*
1067 * The secure url meta tag should always come first, but just in case it doesn't, make sure
1068 * the url value has not already been added.
1069 */
1070 if ( $last_url !== $value ) { // Just in case.
1071
1072 if ( $value ) {
1073
1074 $name_secure_url = $name;
1075 $name_url = str_replace( ':secure_url', ':url', $name );
1076 $name_no_url = str_replace( ':secure_url', '', $name );
1077
1078 if ( SucomUtil::is_https( $value ) ) {
1079
1080 $singles[] = array( '', $tag, $type, $name_secure_url, $attr, $value, $cmt );
1081 }
1082
1083 $singles[] = array( '', $tag, $type, $name_url, $attr, $value, $cmt );
1084
1085 $singles[] = array( '', $tag, $type, $name_no_url, $attr, $value, $cmt );
1086
1087 $last_secure_url = $value;
1088
1089 if ( $this->p->debug->enabled ) {
1090
1091 $this->p->debug->log_arr( 'singles', $singles );
1092 }
1093 }
1094 }
1095
1096 $last_url = null;
1097
1098 break;
1099
1100 case 'og:image:url':
1101 case 'og:video:url':
1102
1103 /*
1104 * The previous switch block would have set all three meta tags already, so only proceed if
1105 * the last secure url (empty or not) is not equal to the current url value (empty or not).
1106 */
1107 if ( $last_secure_url !== $value ) { // Just in case.
1108
1109 if ( $value ) {
1110
1111 $name_secure_url = str_replace( ':url', ':secure_url', $name );
1112 $name_url = $name;
1113 $name_no_url = str_replace( ':url', '', $name );
1114
1115 if ( SucomUtil::is_https( $value ) ) {
1116
1117 $singles[] = array( '', $tag, $type, $name_secure_url, $attr, $value, $cmt );
1118 }
1119
1120 $singles[] = array( '', $tag, $type, $name_url, $attr, $value, $cmt );
1121
1122 $singles[] = array( '', $tag, $type, $name_no_url, $attr, $value, $cmt );
1123
1124 $last_url = $value;
1125
1126 if ( $this->p->debug->enabled ) {
1127
1128 $this->p->debug->log_arr( 'singles', $singles );
1129 }
1130 }
1131 }
1132
1133 $last_secure_url = null;
1134
1135 break;
1136
1137 case 'og:image':
1138 case 'og:video':
1139
1140 /*
1141 * The previous two switch blocks would have set all three meta tags aready, so only
1142 * proceed if we have a value, and it does not match the last secure url (empty or not) or
1143 * the last insecure url (empty or not).
1144 */
1145 if ( empty( $value ) || $value === $last_secure_url || $value === $last_url ) {
1146
1147 break;
1148 }
1149
1150 // No break.
1151
1152 default:
1153
1154 $singles[] = array( '', $tag, $type, $name, $attr, $value, $cmt );
1155
1156 break;
1157 }
1158
1159
1160 /*
1161 * $parts = array( $html, $tag, $type, $name, $attr, $value, $cmt );
1162 */
1163 foreach ( $singles as $num => $parts ) {
1164
1165 if ( ! array_key_exists( 6, $parts ) ) { // Just in case - check for missing $value element.
1166
1167 if ( $this->p->debug->enabled ) {
1168
1169 $this->p->debug->log( 'parts array is incomplete (skipped)' );
1170
1171 $this->p->debug->log_arr( 'parts', $parts );
1172 }
1173
1174 continue;
1175 }
1176
1177 $log_prefix = $parts[ 1 ] . ' ' . $parts[ 2 ] . ' ' . $parts[ 3 ];
1178
1179 if ( $this->p->debug->enabled ) {
1180
1181 $this->p->debug->log( $log_prefix . ' = "' . $parts[ 5 ] . '"' );
1182 }
1183
1184 if ( '' === $parts[ 5 ] || null === $parts[ 5 ] ) { // Allow for 0.
1185
1186 if ( $this->p->debug->enabled ) {
1187
1188 $this->p->debug->log( $log_prefix . ' skipped: value is empty' );
1189 }
1190
1191 $singles[ $num ][ 5 ] = ''; // Avoid null values in REST API output.
1192
1193 continue;
1194
1195 } elseif ( $parts[ 5 ] === WPSSO_UNDEF || $parts[ 5 ] === (string) WPSSO_UNDEF ) {
1196
1197 if ( $this->p->debug->enabled ) {
1198
1199 $this->p->debug->log( $log_prefix . ' skipped: value is ' . WPSSO_UNDEF );
1200 }
1201
1202 continue;
1203 }
1204
1205 /*
1206 * Encode and escape all values, regardless if the head tag is enabled or not. If the head tag is
1207 * enabled, HTML will be created and saved in $parts[ 0 ].
1208 */
1209 if ( 'itemprop' === $parts[ 2 ] && 0 !== strpos( $parts[ 3 ], '.' ) ) {
1210
1211 $match_name = preg_replace( '/^.*\./', '', $parts[ 3 ] );
1212
1213 } else $match_name = $parts[ 3 ];
1214
1215 /*
1216 * Boolean values are converted to their string equivalent.
1217 */
1218 if ( is_bool( $parts[ 5 ] ) ) {
1219
1220 $parts[ 5 ] = $parts[ 5 ] ? 'true' : 'false';
1221 }
1222
1223 switch ( $match_name ) {
1224
1225 /*
1226 * Description values that may include emoji.
1227 */
1228 case 'og:title':
1229 case 'og:description':
1230 case 'og:image:alt':
1231 case 'twitter:title':
1232 case 'twitter:description':
1233 case 'twitter:image:alt':
1234 case 'description':
1235 case 'name':
1236
1237 $parts[ 5 ] = SucomUtil::encode_html_emoji( $parts[ 5 ] ); // Does not double-encode.
1238
1239 break;
1240
1241 /*
1242 * URL values that must be URL encoded.
1243 */
1244 case 'og:secure_url':
1245 case 'og:url':
1246 case 'og:image:secure_url':
1247 case 'og:image:url':
1248 case 'og:image':
1249 case 'og:video:embed_url':
1250 case 'og:video:secure_url':
1251 case 'og:video:url':
1252 case 'og:video':
1253 case 'place:business:menu_url':
1254 case 'place:business:order_url':
1255 case 'twitter:image':
1256 case 'twitter:player':
1257 case 'canonical':
1258 case 'shortlink':
1259 case 'image':
1260 case 'hasmenu': // Food establishment menu url.
1261 case 'url':
1262
1263 $parts[ 5 ] = SucomUtil::esc_url_encode( $parts[ 5 ] );
1264
1265 if ( $parts[ 2 ] === 'itemprop' ) { // An itemprop URL must be a 'link'.
1266
1267 $parts[ 1 ] = 'link';
1268 $parts[ 4 ] = 'href';
1269 }
1270
1271 break;
1272
1273 /*
1274 * Allow for mobile app / non-standard protocols.
1275 */
1276 case 'al:android:url':
1277 case 'al:ios:url':
1278 case 'al:web:url':
1279 case 'twitter:app:url:iphone':
1280 case 'twitter:app:url:ipad':
1281 case 'twitter:app:url:googleplay':
1282
1283 $parts[ 5 ] = SucomUtil::esc_url_encode( $parts[ 5 ], $esc_url = false );
1284
1285 break;
1286
1287 /*
1288 * Encode html entities for everything else.
1289 */
1290 default:
1291
1292 $parts[ 5 ] = htmlentities( $parts[ 5 ], ENT_QUOTES, $charset, $double_encode = false );
1293
1294 break;
1295 }
1296
1297 /*
1298 * Convert mixed case itemprop names to lower case.
1299 */
1300 $opt_key = strtolower( 'add_' . $parts[ 1 ] . '_' . $parts[ 2 ] . '_' . $parts[ 3 ] );
1301
1302 if ( ! empty( $this->p->options[ $opt_key ] ) || 0 === strpos( $match_name, 'wpsso-' ) ) {
1303
1304 $parts_prefix = empty( $parts[ 6 ] ) ? '' : '<!-- ' . $parts[ 6 ] . ' -->';
1305
1306 $parts[ 0 ] = $parts_prefix . '<' . $parts[ 1 ] . ' ' . $parts[ 2 ] . '="' . $match_name . '" ' .
1307 $parts[ 4 ] . '="' . $parts[ 5 ] . '"/>' . "\n";
1308
1309 } elseif ( $this->p->debug->enabled ) {
1310
1311 if ( isset( $this->p->options[ $opt_key ] ) ) {
1312
1313 $this->p->debug->log( $log_prefix . ' skipped: option is disabled' );
1314
1315 } else $this->p->debug->log( $log_prefix . ' skipped: internal meta tag' );
1316 }
1317
1318 $mt_array[] = $parts; // Save the HTML and encoded value.
1319 }
1320
1321 unset( $singles, $num, $parts );
1322
1323 return $mt_array;
1324 }
1325 }
1326 }
1327