PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / 22.7.0
WPSSO Core – Complete Schema Markup and Meta Tags v22.7.0
22.7.0 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
← All changes | lib/opengraph.php +1729 -0 22.1.022.7.0 View file →
@@ -1,0 +1,1729 @@
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( 'WpssoOpenGraph' ) ) {
19 +
20 + class WpssoOpenGraph {
21 +
22 + private $p; // Wpsso class object.
23 + private $ns; // WpssoOpenGraphNS class object.
24 +
25 + public function __construct( &$plugin ) {
26 +
27 + $this->p =& $plugin;
28 +
29 + if ( $this->p->debug->enabled ) {
30 +
31 + $this->p->debug->mark();
32 + }
33 +
34 + /*
35 + * Instantiate the WpssoOpenGraphNS class object.
36 + */
37 + if ( ! class_exists( 'WpssoOpenGraphNS' ) ) {
38 +
39 + require_once WPSSO_PLUGINDIR . 'lib/opengraph-ns.php';
40 + }
41 +
42 + $this->ns = new WpssoOpenGraphNS( $plugin );
43 +
44 + $this->p->util->add_plugin_filters( $this, array(
45 + 'plugin_image_sizes' => 1,
46 + ) );
47 +
48 + $this->p->util->add_plugin_filters( $this, array(
49 + 'get_post_options' => 3,
50 + ), PHP_INT_MAX );
51 + }
52 +
53 + public function filter_plugin_image_sizes( array $sizes ) {
54 +
55 + $sizes[ 'og' ] = array( // Option prefix.
56 + 'name' => 'opengraph',
57 + 'label_transl' => _x( 'Open Graph (Facebook and oEmbed)', 'option label', 'wpsso' ),
58 + );
59 +
60 + return $sizes;
61 + }
62 +
63 + public function filter_get_post_options( array $md_opts, $post_id, array $mod ) {
64 +
65 + if ( $this->p->debug->enabled ) {
66 +
67 + $this->p->debug->mark();
68 + }
69 +
70 + if ( is_admin() ) { // Keep processing on the front-end to a minimum.
71 +
72 + /*
73 + * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and
74 + * the Schema type, check for a possible hard-coded Open Graph type.
75 + */
76 + $md_opts = $this->maybe_update_post_og_type( $md_opts, $post_id, $mod );
77 + }
78 +
79 + return $md_opts;
80 + }
81 +
82 + /*
83 + * Returns the open graph type id.
84 + */
85 + public function get_mod_og_type_id( array $mod, $use_md_opts = true ) {
86 +
87 + return $this->get_mod_og_type( $mod, $get_id = true, $use_md_opts );
88 + }
89 +
90 + /*
91 + * Returns the open graph namespace.
92 + */
93 + public function get_mod_og_type_ns( array $mod, $use_md_opts = true ) {
94 +
95 + return $this->get_mod_og_type( $mod, $get_id = false, $use_md_opts );
96 + }
97 +
98 + /*
99 + * Returns the open graph type id by default.
100 + *
101 + * Example: article, product, place, etc.
102 + *
103 + * Use $get_id = false to return the open graph namespace instead of the ID.
104 + */
105 + public function get_mod_og_type( array $mod, $get_id = true, $use_md_opts = true ) {
106 +
107 + if ( $this->p->debug->enabled ) {
108 +
109 + $this->p->debug->mark();
110 + }
111 +
112 + static $local_cache = array();
113 +
114 + $cache_salt = false;
115 +
116 + /*
117 + * Archive pages can call this method several times.
118 + *
119 + * Optimize and cache post/term/user og type values.
120 + */
121 + if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
122 +
123 + /*
124 + * Note that the sort order, page number, locale, amp and embed checks are provided by
125 + * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
126 + */
127 + $cache_salt = SucomUtil::get_mod_salt( $mod ) . '_get:' . (string) $get_id . '_use:' . (string) $use_md_opts;
128 +
129 + if ( isset( $local_cache[ $cache_salt ] ) ) {
130 +
131 + return $local_cache[ $cache_salt ];
132 + }
133 + }
134 +
135 + $type_id = null;
136 + $og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ];
137 +
138 + /*
139 + * Maybe get a custom open graph type id from the post, term, or user meta.
140 + */
141 + if ( $use_md_opts ) {
142 +
143 + if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
144 +
145 + $type_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'og_type' ); // Returns null if index key not found.
146 +
147 + if ( empty( $type_id ) || $type_id === 'none' || empty( $og_type_ns[ $type_id ] ) ) { // Check for an invalid type id.
148 +
149 + $type_id = null;
150 +
151 + } elseif ( $this->p->debug->enabled ) {
152 +
153 + $this->p->debug->log( 'custom type id = ' . $type_id );
154 + }
155 + }
156 + }
157 +
158 + $is_custom = empty( $type_id ) ? false : true;
159 +
160 + if ( ! $is_custom ) { // No custom open graph type id from the post, term, or user meta.
161 +
162 + /*
163 + * Similar module type logic can be found in the following methods:
164 + *
165 + * See WpssoOpenGraph->get_mod_og_type().
166 + * See WpssoPage->get_description().
167 + * See WpssoPage->get_the_title().
168 + * See WpssoSchema->get_mod_schema_type().
169 + * See WpssoUtil->get_canonical_url().
170 + */
171 + if ( $mod[ 'is_home' ] ) { // Home page (static or blog archive).
172 +
173 + if ( $mod[ 'is_home_page' ] ) { // Static front page (singular post).
174 +
175 + $type_id = $this->get_og_type_id( 'home_page' );
176 +
177 + } else $type_id = $this->get_og_type_id( 'home_posts' );
178 +
179 + } elseif ( $mod[ 'is_comment' ] ) {
180 +
181 + if ( is_numeric( $mod[ 'comment_rating' ] ) ) {
182 +
183 + $type_id = $this->get_og_type_id( 'comment_review' );
184 +
185 + } elseif ( $mod[ 'comment_parent' ] ) {
186 +
187 + $type_id = $this->get_og_type_id( 'comment_reply' );
188 +
189 + } else $type_id = $this->get_og_type_id( 'comment' );
190 +
191 + } elseif ( $mod[ 'is_post' ] ) {
192 +
193 + if ( ! empty( $mod[ 'post_type' ] ) && is_string( $mod[ 'post_type' ] ) ) { // Not false or empty string.
194 +
195 + if ( $mod[ 'is_post_type_archive' ] ) { // The post ID may be 0.
196 +
197 + $type_id = $this->get_og_type_id( 'pta_' . $mod[ 'post_type' ] );
198 +
199 + if ( empty( $type_id ) ) { // Just in case.
200 +
201 + $type_id = $this->get_og_type_id( 'archive_page' );
202 + }
203 +
204 + } else {
205 +
206 + $type_id = $this->get_og_type_id( $mod[ 'post_type' ] );
207 +
208 + if ( empty( $type_id ) ) { // Just in case.
209 +
210 + $type_id = $this->get_og_type_id( 'page' );
211 + }
212 + }
213 +
214 + } elseif ( $this->p->debug->enabled ) {
215 +
216 + $this->p->debug->log( 'no post type' );
217 + }
218 +
219 + } elseif ( $mod[ 'is_term' ] ) {
220 +
221 + if ( ! empty( $mod[ 'tax_slug' ] ) ) { // Just in case.
222 +
223 + $type_id = $this->get_og_type_id( 'tax_' . $mod[ 'tax_slug' ] );
224 + }
225 +
226 + if ( empty( $type_id ) ) { // Just in case.
227 +
228 + $type_id = $this->get_og_type_id( 'archive_page' );
229 + }
230 +
231 + } elseif ( $mod[ 'is_user' ] ) {
232 +
233 + $type_id = $this->get_og_type_id( 'user_page' );
234 +
235 + } elseif ( $mod[ 'is_search' ] ) {
236 +
237 + $type_id = $this->get_og_type_id( 'search_page' );
238 +
239 + } elseif ( $mod[ 'is_archive' ] ) {
240 +
241 + $type_id = $this->get_og_type_id( 'archive_page' );
242 + }
243 +
244 + if ( empty( $type_id ) ) { // Just in case.
245 +
246 + if ( $this->p->debug->enabled ) {
247 +
248 + $this->p->debug->log( 'unable to determine og type id (using default)' );
249 + }
250 +
251 + $type_id = 'website';
252 + }
253 + }
254 +
255 + if ( $this->p->debug->enabled ) {
256 +
257 + $this->p->debug->log( 'og type id before filter = ' . $type_id );
258 + }
259 +
260 + $type_id = apply_filters( 'wpsso_og_type', $type_id, $mod, $is_custom );
261 +
262 + if ( $this->p->debug->enabled ) {
263 +
264 + $this->p->debug->log( 'og type id after filter = ' . $type_id );
265 + }
266 +
267 + $get_value = false;
268 +
269 + if ( empty( $type_id ) ) {
270 +
271 + if ( $this->p->debug->enabled ) {
272 +
273 + $this->p->debug->log( 'returning false: og type id is empty' );
274 + }
275 +
276 + } elseif ( 'none' === $type_id ) {
277 +
278 + if ( $this->p->debug->enabled ) {
279 +
280 + $this->p->debug->log( 'returning false: og type id is disabled' );
281 + }
282 +
283 + } elseif ( ! isset( $og_type_ns[ $type_id ] ) ) {
284 +
285 + if ( $this->p->debug->enabled ) {
286 +
287 + $this->p->debug->log( 'returning false: og type id ' . $type_id . ' is unknown' );
288 + }
289 +
290 + } elseif ( ! $get_id ) {
291 +
292 + if ( $this->p->debug->enabled ) {
293 +
294 + $this->p->debug->log( 'returning og type id namespace: ' . $og_type_ns[ $type_id ] );
295 + }
296 +
297 + $get_value = $og_type_ns[ $type_id ];
298 +
299 + } else {
300 +
301 + if ( $this->p->debug->enabled ) {
302 +
303 + $this->p->debug->log( 'returning og type id: ' . $type_id );
304 + }
305 +
306 + $get_value = $type_id;
307 + }
308 +
309 + /*
310 + * Optimize and cache post/term/user og type values.
311 + */
312 + if ( $cache_salt ) {
313 +
314 + $local_cache[ $cache_salt ] = $get_value;
315 + }
316 +
317 + return $get_value;
318 + }
319 +
320 + /*
321 + * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
322 + *
323 + * $size_name is passed as-is to WpssoMedia->get_all_images().
324 + */
325 + public function get_array( array $mod, $size_names = 'opengraph', $md_pre = 'og' ) {
326 +
327 + if ( $this->p->debug->enabled ) {
328 +
329 + $this->p->debug->mark();
330 + }
331 +
332 + $max_nums = $this->p->util->get_max_nums( $mod );
333 +
334 + if ( $this->p->debug->enabled ) {
335 +
336 + $this->p->debug->mark( 'applying filters "wpsso_og_seed"' ); // Begin timer.
337 + }
338 +
339 + /*
340 + * 'wpsso_og_seed' is hooked by e-commerce modules to provide product meta tags.
341 + */
342 + $mt_og = apply_filters( 'wpsso_og_seed', SucomUtil::get_mt_og_seed(), $mod );
343 +
344 + if ( $this->p->debug->enabled ) {
345 +
346 + $this->p->debug->log_arr( 'og_seed', $mt_og );
347 +
348 + $this->p->debug->mark( 'applying filters "wpsso_og_seed"' ); // End timer.
349 + }
350 +
351 + /*
352 + * Facebook app id meta tag.
353 + */
354 + if ( ! isset( $mt_og[ 'fb:app_id' ] ) ) {
355 +
356 + $mt_og[ 'fb:app_id' ] = $this->p->options[ 'fb_app_id' ];
357 + }
358 +
359 + /*
360 + * Current ISO 8601 date and time (non-standard / internal meta tag).
361 + */
362 + $mt_og[ 'og:time' ] = gmdate( 'c' );
363 +
364 + /*
365 + * Type id meta tag.
366 + */
367 + if ( ! isset( $mt_og[ 'og:type' ] ) ) {
368 +
369 + $mt_og[ 'og:type' ] = $this->get_mod_og_type_id( $mod );
370 +
371 + } elseif ( $this->p->debug->enabled ) {
372 +
373 + $this->p->debug->log( 'og:type already defined = ' . $mt_og[ 'og:type' ] );
374 + }
375 +
376 + $type_id = $mt_og[ 'og:type' ];
377 +
378 + /*
379 + * URL meta tag.
380 + */
381 + if ( ! isset( $mt_og[ 'og:url' ] ) ) {
382 +
383 + $mt_og[ 'og:url' ] = $this->p->util->get_canonical_url( $mod );
384 +
385 + } elseif ( $this->p->debug->enabled ) {
386 +
387 + $this->p->debug->log( 'og:url already defined = ' . $mt_og[ 'og:url' ] );
388 + }
389 +
390 + /*
391 + * Redirect URL meta tag (non-standard / internal meta tag).
392 + */
393 + $mt_og[ 'og:redirect_url' ] = $this->p->util->get_redirect_url( $mod );
394 +
395 + /*
396 + * Locale meta tag.
397 + */
398 + if ( ! isset( $mt_og[ 'og:locale' ] ) ) {
399 +
400 + if ( $this->p->debug->enabled ) {
401 +
402 + $this->p->debug->log( 'calling get_fb_locale()' );
403 + }
404 +
405 + $mt_og[ 'og:locale' ] = $this->get_fb_locale( $mod );
406 +
407 + } elseif ( $this->p->debug->enabled ) {
408 +
409 + $this->p->debug->log( 'og:locale already defined = ' . $mt_og[ 'og:locale' ] );
410 + }
411 +
412 + /*
413 + * Site name meta tag.
414 + */
415 + if ( ! isset( $mt_og[ 'og:site_name' ] ) ) {
416 +
417 + if ( $this->p->debug->enabled ) {
418 +
419 + $this->p->debug->log( 'getting site name for og:site_name meta tag' );
420 + }
421 +
422 + $mt_og[ 'og:site_name' ] = SucomUtilWP::get_site_name( $this->p->options, $mod ); // localized
423 +
424 + } elseif ( $this->p->debug->enabled ) {
425 +
426 + $this->p->debug->log( 'og:site_name already defined = ' . $mt_og[ 'og:site_name' ] );
427 + }
428 +
429 + /*
430 + * Title meta tag.
431 + */
432 + if ( ! isset( $mt_og[ 'og:title' ] ) ) {
433 +
434 + if ( $this->p->debug->enabled ) {
435 +
436 + $this->p->debug->log( 'getting title for og:title meta tag' );
437 + }
438 +
439 + $mt_og[ 'og:title' ] = $this->p->page->get_title( $mod, $md_key = 'og_title', $max_len = 'og_title' );
440 +
441 + if ( $this->p->debug->enabled ) {
442 +
443 + $this->p->debug->log( 'og:title value = ' . $mt_og[ 'og:title' ] );
444 + }
445 +
446 + } elseif ( $this->p->debug->enabled ) {
447 +
448 + $this->p->debug->log( 'og:title already defined = ' . $mt_og[ 'og:title' ] );
449 + }
450 +
451 + /*
452 + * Description meta tag.
453 + */
454 + if ( ! isset( $mt_og[ 'og:description' ] ) ) {
455 +
456 + if ( $this->p->debug->enabled ) {
457 +
458 + $this->p->debug->log( 'getting description for og:description meta tag' );
459 + }
460 +
461 + $mt_og[ 'og:description' ] = $this->p->page->get_description( $mod, $md_key = 'og_desc', $max_len = 'og_desc', $num_hashtags = true );
462 +
463 + if ( $this->p->debug->enabled ) {
464 +
465 + $this->p->debug->log( 'og:description value = ' . $mt_og[ 'og:description' ] );
466 + }
467 +
468 + } elseif ( $this->p->debug->enabled ) {
469 +
470 + $this->p->debug->log( 'og:description already defined = ' . $mt_og[ 'og:description' ] );
471 + }
472 +
473 + /*
474 + * Updated date / time meta tag.
475 + */
476 + if ( ! isset( $mt_og[ 'og:updated_time' ] ) ) {
477 +
478 + if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) { // ISO 8601 date or false.
479 +
480 + $mt_og[ 'og:updated_time' ] = $mod[ 'post_modified_time' ];
481 + }
482 + }
483 +
484 + /*
485 + * Get all videos.
486 + *
487 + * Call before getting all images to find / use preview images.
488 + */
489 + if ( ! isset( $mt_og[ 'og:video' ] ) ) {
490 +
491 + if ( $this->p->debug->enabled ) {
492 +
493 + $this->p->debug->log( 'getting videos for og:video meta tag' );
494 + }
495 +
496 + if ( $max_nums[ 'og_vid_max' ] > 0 ) {
497 +
498 + $mt_og[ 'og:video' ] = $this->p->media->get_all_videos( $max_nums[ 'og_vid_max' ], $mod, $md_pre );
499 +
500 + if ( empty( $mt_og[ 'og:video' ] ) ) {
501 +
502 + if ( $this->p->debug->enabled ) {
503 +
504 + $this->p->debug->log( 'no videos returned' );
505 + }
506 +
507 + unset( $mt_og[ 'og:video' ] );
508 +
509 + } else {
510 +
511 + if ( $this->p->debug->enabled ) {
512 +
513 + $this->p->debug->log( 'removing video images to avoid duplicates' );
514 + }
515 +
516 + foreach ( $mt_og[ 'og:video' ] as $key => $mt_single ) {
517 +
518 + if ( ! is_array( $mt_single ) ) { // Just in case.
519 +
520 + if ( $this->p->debug->enabled ) {
521 +
522 + $this->p->debug->log( 'video ignored: $mt_single is not an array' );
523 + }
524 +
525 + continue;
526 + }
527 +
528 + $mt_og[ 'og:video' ][ $key ] = SucomUtil::preg_grep_keys( '/^og:image/', $mt_single, $invert = true );
529 + }
530 + }
531 +
532 + } elseif ( $this->p->debug->enabled ) {
533 +
534 + $this->p->debug->log( 'videos disabled: maximum videos is 0 or less' );
535 + }
536 + }
537 +
538 + /*
539 + * Get all images.
540 + */
541 + if ( ! isset( $mt_og[ 'og:image' ] ) ) {
542 +
543 + if ( $this->p->debug->enabled ) {
544 +
545 + $this->p->debug->log( 'getting images for og:image meta tag' );
546 + }
547 +
548 + if ( $max_nums[ 'og_img_max' ] > 0 ) {
549 +
550 + $mt_og[ 'og:image' ] = $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre );
551 +
552 + if ( empty( $mt_og[ 'og:image' ] ) ) {
553 +
554 + if ( $this->p->debug->enabled ) {
555 +
556 + $this->p->debug->log( 'no images returned' );
557 + }
558 +
559 + unset( $mt_og[ 'og:image' ] );
560 + }
561 +
562 + } else {
563 +
564 + if ( $this->p->debug->enabled ) {
565 +
566 + $this->p->debug->log( 'skipped getting images: maximum images is 0 or less' );
567 + }
568 + }
569 + }
570 +
571 + /*
572 + * Pre-define some basic open graph meta tags for this og:type. If the meta tag has an associated meta
573 + * option name, then read it's value from the meta options.
574 + */
575 + if ( $this->p->debug->enabled ) {
576 +
577 + $this->p->debug->log( 'checking og_type_mt array for known meta tags and md options' );
578 + }
579 +
580 + /*
581 + * An array of Open Graph types, their meta tags, and their associated metadata keys.
582 + */
583 + if ( isset( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) { // Check if og:type is known.
584 +
585 + /*
586 + * Optimize and call get_options() only once. Returns an empty string if no meta found.
587 + */
588 + if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
589 +
590 + $md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_key = false, $filter_opts = true, $merge_defs = true );
591 +
592 + } else $md_opts = array();
593 +
594 + /*
595 + * Add post/term/user meta data to the Open Graph meta tags.
596 + */
597 + $this->add_data_og_type_md( $mt_og, $type_id, $md_opts );
598 + }
599 +
600 + /*
601 + * If the module is a post object, define the author, publishing date, etc. These values may still be used
602 + * by other non-article filters, and if the og:type is not an article, the meta tags will be sanitized (ie.
603 + * non-valid meta tags removed) at the end of WpssoHead->get_head_array().
604 + */
605 + if ( ! isset( $mt_og[ 'article:author' ] ) ) {
606 +
607 + if ( $this->p->debug->enabled ) {
608 +
609 + $this->p->debug->log( 'getting names / URLs for article:author meta tags' );
610 + }
611 +
612 + $mt_og[ 'article:author' ] = array();
613 +
614 + if ( $mod[ 'is_post' ] ) {
615 +
616 + if ( $mod[ 'post_author' ] ) {
617 +
618 + $mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'post_author' ], 'display_name' );
619 + $mt_og[ 'article:author' ] = $this->p->user->get_authors_websites( $mod[ 'post_author' ], $meta_key = 'facebook' );
620 + }
621 +
622 + if ( $mod[ 'post_coauthors' ] ) {
623 +
624 + $og_profile_urls = $this->p->user->get_authors_websites( $mod[ 'post_coauthors' ], $meta_key = 'facebook' );
625 +
626 + $mt_og[ 'article:author' ] = array_merge( $mt_og[ 'article:author' ], $og_profile_urls );
627 + }
628 +
629 + } elseif ( $mod[ 'is_comment' ] ) {
630 +
631 + if ( $mod[ 'comment_author' ] ) {
632 +
633 + $mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'comment_author' ], 'display_name' );
634 + $mt_og[ 'article:author' ] = $this->p->user->get_authors_websites( $mod[ 'comment_author' ], $meta_key = 'facebook' );
635 +
636 + } elseif ( $mod[ 'comment_author_name' ] ) {
637 +
638 + $mt_og[ 'article:author:name' ] = $mod[ 'comment_author_name' ];
639 + }
640 + }
641 + }
642 +
643 + if ( ! isset( $mt_og[ 'article:publisher' ] ) ) {
644 +
645 + $mt_og[ 'article:publisher' ] = SucomUtilOptions::get_key_value( 'fb_publisher_url', $this->p->options, $mod );
646 + }
647 +
648 + if ( ! isset( $mt_og[ 'article:tag' ] ) ) {
649 +
650 + $mt_og[ 'article:tag' ] = $this->p->page->get_tag_names( $mod );
651 + }
652 +
653 + if ( ! isset( $mt_og[ 'article:published_time' ] ) ) {
654 +
655 + if ( $mod[ 'is_post' ] && $mod[ 'post_time' ] ) { // ISO 8601 date or false.
656 +
657 + switch ( $mod[ 'post_status' ] ) {
658 +
659 + case 'auto-draft':
660 + case 'draft':
661 + case 'future':
662 + case 'inherit': // Post revision.
663 + case 'pending':
664 + case 'trash':
665 +
666 + if ( $this->p->debug->enabled ) {
667 +
668 + $this->p->debug->log( 'skipping article published time for post status ' . $mod[ 'post_status' ] );
669 + }
670 +
671 + break;
672 +
673 + case 'expired': // Previously published.
674 + case 'private':
675 + case 'publish':
676 + default: // Any other post status.
677 +
678 + $mt_og[ 'article:published_time' ] = $mod[ 'post_time' ];
679 +
680 + break;
681 + }
682 +
683 + } elseif ( $mod[ 'is_comment' ] && $mod[ 'comment_time' ] ) { // ISO 8601 date or false.
684 +
685 + $mt_og[ 'article:published_time' ] = $mod[ 'comment_time' ];
686 + }
687 + }
688 +
689 + if ( ! isset( $mt_og[ 'article:modified_time' ] ) ) {
690 +
691 + if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) { // ISO 8601 date or false.
692 +
693 + $mt_og[ 'article:modified_time' ] = $mod[ 'post_modified_time' ];
694 + }
695 + }
696 +
697 + if ( ! isset( $mt_og[ 'article:reading_mins' ] ) ) {
698 +
699 + $mt_og[ 'article:reading_mins' ] = $this->p->page->get_reading_mins( $mod );
700 + }
701 +
702 + if ( ! empty( $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ] ) ) {
703 +
704 + $og_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ]; // Example: https://ogp.me/ns/product#
705 +
706 + $filter_name = 'wpsso_og_data_' . SucomUtil::sanitize_hookname( $og_ns );
707 +
708 + $mt_og = apply_filters( $filter_name, $mt_og, $mod );
709 + }
710 +
711 + if ( $this->p->debug->enabled ) {
712 +
713 + $this->p->debug->log( 'applying filters "wpsso_og"' );
714 + }
715 +
716 + $mt_og = apply_filters( 'wpsso_og', $mt_og, $mod );
717 +
718 + /*
719 + * Expand inline variables.
720 + */
721 + $mt_og = $this->p->util->inline->replace_variables( $mt_og, $mod );
722 +
723 + if ( $this->p->debug->enabled ) {
724 +
725 + $this->p->debug->log_arr( 'mt_og', $mt_og );
726 + }
727 +
728 + return $mt_og;
729 + }
730 +
731 + public function get_og_type_id( $opt_suffix, $default_id = false ) {
732 +
733 + if ( $this->p->debug->enabled ) {
734 +
735 + $this->p->debug->log_args( array(
736 + 'opt_suffix' => $opt_suffix,
737 + 'default_id' => $default_id,
738 + ) );
739 + }
740 +
741 + if ( empty( $opt_suffix ) ) { // Just in case.
742 +
743 + if ( $this->p->debug->enabled ) {
744 +
745 + $this->p->debug->log( 'exiting early: opt_suffix is empty' );
746 + }
747 +
748 + return $default_id;
749 +
750 + } elseif ( ! is_string( $opt_suffix ) ) { // Must be a string.
751 +
752 + if ( $this->p->debug->enabled ) {
753 +
754 + $this->p->debug->log( 'exiting early: opt_suffix is ' . gettype( $opt_suffix ) );
755 + }
756 +
757 + return $default_id;
758 + }
759 +
760 + $og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ];
761 + $opt_key = 'og_type_for_' . $opt_suffix;
762 + $type_id = isset( $this->p->options[ $opt_key ] ) ? $this->p->options[ $opt_key ] : $default_id;
763 +
764 + if ( empty( $type_id ) || 'none' === $type_id || empty( $og_type_ns[ $type_id ] ) ) { // Invalid type id.
765 +
766 + if ( empty( $default_id ) || 'none' === $default_id || empty( $og_type_ns[ $default_id ] ) ) { // Invalid default id.
767 +
768 + return false;
769 + }
770 +
771 + return $default_id;
772 + }
773 +
774 + return $type_id;
775 + }
776 +
777 + public function get_og_types() {
778 +
779 + if ( $this->p->debug->enabled ) {
780 +
781 + $this->p->debug->mark();
782 + }
783 +
784 + return $this->p->cf[ 'head' ][ 'og_type_ns_compat' ];
785 + }
786 +
787 + public function get_og_types_select() {
788 +
789 + if ( $this->p->debug->enabled ) {
790 +
791 + $this->p->debug->mark();
792 + }
793 +
794 + $og_type_ns = $this->get_og_types();
795 +
796 + $select = array();
797 +
798 + foreach ( $og_type_ns as $type_id => $type_ns ) {
799 +
800 + $type_url = preg_replace( '/(^.*\/\/|#$)/', '', $type_ns );
801 + $type_name = preg_replace( '/(^.*\/ns\/|#$)/U', '', $type_url );
802 +
803 + switch ( $this->p->options[ 'plugin_og_types_select_format' ] ) {
804 +
805 + case 'name': // Options default.
806 +
807 + $select[ $type_id ] = $type_name;
808 +
809 + break;
810 +
811 + case 'name_id':
812 +
813 + $select[ $type_id ] = $type_name . ' [' . $type_id . ']';
814 +
815 + break;
816 +
817 + case 'id':
818 +
819 + $select[ $type_id ] = $type_id;
820 +
821 + break;
822 +
823 + case 'id_url':
824 +
825 + $select[ $type_id ] = $type_id . ' | ' . $type_url;
826 +
827 + break;
828 +
829 + case 'id_name':
830 +
831 + $select[ $type_id ] = $type_id . ' | ' . $type_name;
832 +
833 + break;
834 +
835 + default:
836 +
837 + $select[ $type_id ] = $type_name;
838 +
839 + break;
840 + }
841 + }
842 +
843 + if ( defined( 'SORT_STRING' ) ) { // Just in case.
844 +
845 + asort( $select, SORT_STRING );
846 +
847 + } else {
848 +
849 + asort( $select );
850 + }
851 +
852 + return $select;
853 + }
854 +
855 + /*
856 + * Returns an optional and customized locale value for the og:locale meta tag.
857 + *
858 + * $mixed = 'default' | 'current' | post ID | $mod array
859 + *
860 + * See WpssoOpenGraph->get_array().
861 + * See WpssoOptions->get_defaults().
862 + */
863 + public function get_fb_locale( $mixed = 'current', $use_opts = true ) {
864 +
865 + if ( $this->p->debug->enabled ) {
866 +
867 + $this->p->debug->mark();
868 + }
869 +
870 + /*
871 + * Maybe get an alternate Facebook locale from the plugin settings.
872 + */
873 + if ( $use_opts ) {
874 +
875 + $key_locale = SucomUtilOptions::get_key_locale( 'fb_locale', $this->p->options, $mixed );
876 +
877 + if ( ! empty( $this->p->options[ $key_locale ] ) ) { // Maybe use an alternate locale value.
878 +
879 + $fb_locale = $this->p->options[ $key_locale ];
880 +
881 + if ( $this->p->debug->enabled ) {
882 +
883 + $this->p->debug->log( 'returning fb locale "' . $fb_locale . '" from ' . $key_locale . ' options key' );
884 + }
885 +
886 + return $fb_locale;
887 + }
888 + }
889 +
890 + /*
891 + * Maybe return the Facebook equivalent for this locale.
892 + */
893 + if ( $this->p->debug->enabled ) {
894 +
895 + $this->p->debug->log( 'calling get_schema_lang()' );
896 + }
897 +
898 + $schema_lang = $this->p->schema->get_schema_lang( $mixed );
899 +
900 + $fb_languages = SucomUtil::get_publisher_languages( 'facebook' );
901 +
902 + /*
903 + * Fix known exceptions.
904 + */
905 + switch ( $schema_lang ) {
906 +
907 + case 'en_UK':
908 +
909 + $schema_lang = 'en_GB';
910 +
911 + break;
912 +
913 + case 'de_DE_formal':
914 +
915 + $schema_lang = 'de_DE';
916 +
917 + break;
918 + }
919 +
920 + if ( ! empty( $fb_languages[ $schema_lang ] ) ) {
921 +
922 + if ( $this->p->debug->enabled ) {
923 +
924 + $this->p->debug->log( 'returning schema language "' . $schema_lang . '"' );
925 + }
926 +
927 + return $schema_lang;
928 + }
929 +
930 + /*
931 + * Maybe fallback to the default locale.
932 + */
933 + $def_locale = $this->p->schema->get_schema_lang( 'default' );
934 +
935 + if ( ! empty( $fb_languages[ $def_locale ] ) ) {
936 +
937 + if ( $this->p->debug->enabled ) {
938 +
939 + $this->p->debug->log( 'returning default locale "' . $def_locale . '"' );
940 + }
941 +
942 + return $def_locale;
943 + }
944 +
945 + /*
946 + * Fallback to en_US.
947 + */
948 + if ( $this->p->debug->enabled ) {
949 +
950 + $this->p->debug->log( 'returning fallback locale "en_US"' );
951 + }
952 +
953 + return 'en_US';
954 + }
955 +
956 + /*
957 + * Since WPSSO Core v15.0.0.
958 + *
959 + * See WpssoCmcfActions->check_product_image_urls().
960 + * See WpssoCmcfXml->add_product_images().
961 + * See WpssoGmfActions->check_product_image_urls().
962 + * See WpssoGmfXml->add_product_images().
963 + */
964 + public function get_product_retailer_item_image_urls( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) {
965 +
966 + $image_urls = array();
967 + $mt_images = $this->get_product_retailer_item_images( $mt_single, $size_names, $md_pre );
968 +
969 + if ( is_array( $mt_images ) ) { // Just in case.
970 +
971 + foreach ( $mt_images as $mt_image ) {
972 +
973 + if ( $image_url = SucomUtil::get_first_og_image_url( $mt_image ) ) {
974 +
975 + $image_urls[] = $image_url;
976 + }
977 + }
978 + }
979 +
980 + return $image_urls;
981 + }
982 +
983 + /*
984 + * Since WPSSO Core v15.0.0.
985 + *
986 + * WpssoOpenGraph->get_product_retailer_item_image_urls().
987 + */
988 + public function get_product_retailer_item_images( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) {
989 +
990 + if ( isset( $mt_single[ 'og:image' ] ) && is_array( $mt_single[ 'og:image' ] ) ) { // Nothing to do.
991 +
992 + return $mt_single[ 'og:image' ];
993 +
994 + } elseif ( ! $mod = $this->get_product_retailer_item_mod( $mt_single ) ) { // Returns false or array.
995 +
996 + if ( $wpsso->debug->enabled ) {
997 +
998 + $wpsso->debug->log( 'returning early: no post id for retailer item id' );
999 + }
1000 +
1001 + return array();
1002 + }
1003 +
1004 + $max_nums = $this->p->util->get_max_nums( $mod, 'og' ); // $mod must be an array.
1005 +
1006 + return $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre );
1007 + }
1008 +
1009 + /*
1010 + * Since WPSSO Core v15.0.0.
1011 + *
1012 + * See WpssoCmcfActions->check_product_image_urls().
1013 + * See WpssoGmfActions->check_product_image_urls().
1014 + * See WpssoOpenGraph->get_product_retailer_item_images().
1015 + * See WpssoSchema::add_offers_aggregate_data_mt().
1016 + * See WpssoSchema::add_offers_data_mt().
1017 + * See WpssoSchema::add_variants_data_mt().
1018 + */
1019 + public function get_product_retailer_item_mod( array $mt_single ) {
1020 +
1021 + if ( ! empty( $mt_single[ 'product:retailer_item_id' ] ) && is_numeric( $mt_single[ 'product:retailer_item_id' ] ) ) {
1022 +
1023 + $mod = $this->p->post->get_mod( $mt_single[ 'product:retailer_item_id' ] );
1024 +
1025 + return empty( $mod[ 'id' ] ) ? false : $mod; // Just in case.
1026 + }
1027 +
1028 + return false;
1029 + }
1030 +
1031 + /*
1032 + * Returns a string.
1033 + *
1034 + * Used for the 'product:retailer_category' meta tag value.
1035 + */
1036 + public function get_product_retailer_category( $mod ) {
1037 +
1038 + $retailer_categories = $this->get_product_retailer_categories( $mod, $lists_max = 1 );
1039 +
1040 + return isset( $retailer_categories[ 0 ] ) ? $retailer_categories[ 0 ] : '';
1041 + }
1042 +
1043 + /*
1044 + * Returns an array of strings.
1045 + */
1046 + public function get_product_retailer_categories( $mod, $lists_max = 1 ) {
1047 +
1048 + /*
1049 + * Returns an associative array of term IDs and their names or objects.
1050 + *
1051 + * If the custom primary or default term ID exists in the post terms, it will be moved to the top.
1052 + */
1053 + $post_terms = $this->p->post->get_primary_terms( $mod, $mod[ 'post_primary_tax_slug' ], $output = 'objects' );
1054 +
1055 + $category_names = array();
1056 +
1057 + foreach ( $post_terms as $parent_term_obj ) {
1058 +
1059 + $parent_term_id = $parent_term_obj->term_id;
1060 +
1061 + $ancestor_ids = get_ancestors( $parent_term_id, $mod[ 'post_primary_tax_slug' ], $resource_type = 'taxonomy' );
1062 +
1063 + if ( empty( $ancestor_ids ) || ! is_array( $ancestor_ids ) ) {
1064 +
1065 + $ancestor_ids = array( $parent_term_id ); // Just do the parent term.
1066 +
1067 + } else {
1068 +
1069 + $ancestor_ids = array_reverse( $ancestor_ids ); // Add ancestors in reverse order.
1070 +
1071 + $ancestor_ids[] = $parent_term_id; // Add parent term last.
1072 + }
1073 +
1074 + foreach ( $ancestor_ids as $term_id ) {
1075 +
1076 + $term_mod = $this->p->term->get_mod( $term_id );
1077 +
1078 + /*
1079 + * Use $title_sep = false to avoid adding term parent names in the term title.
1080 + *
1081 + * $md_key = 'schema_title_bc' will use array( 'schema_title_bc', 'schema_title_alt', 'schema_title', 'seo_title' )
1082 + */
1083 + $category_names[ $parent_term_id ][ $term_id ] = $this->p->page->get_title( $term_mod,
1084 + $md_key = 'schema_title_bc', $max_len = 'schema_title_bc', $title_sep = false );
1085 + }
1086 + }
1087 +
1088 + if ( $lists_max ) {
1089 +
1090 + $category_names = array_slice( $category_names, 0, $lists_max );
1091 + }
1092 +
1093 + $retailer_categories = array();
1094 +
1095 + foreach ( $category_names as $parent_term_id => $term_ids ) {
1096 +
1097 + $retailer_categories[] = implode( ' > ', $term_ids );
1098 + }
1099 +
1100 + return $retailer_categories;
1101 + }
1102 +
1103 + /*
1104 + * Returns an array of strings.
1105 + */
1106 + public function get_product_awards( array $mod ) {
1107 +
1108 + $awards = array();
1109 +
1110 + if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {
1111 +
1112 + $md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ] );
1113 +
1114 + if ( is_array( $md_opts ) ) { // Just in case.
1115 +
1116 + $awards = SucomUtil::get_multi_values( $md_opts, 'product_award' );
1117 + }
1118 + }
1119 +
1120 + $awards = apply_filters( 'wpsso_og_product_awards', $awards, $mod );
1121 +
1122 + return $awards;
1123 + }
1124 +
1125 + /*
1126 + * Called by WpssoHead->get_head_array() before merging all meta tag arrays.
1127 + *
1128 + * Unset mis-matched og_type meta tags using the 'og_type_mt' array as a reference. For example, remove all
1129 + * 'article' meta tags if the og_type is 'website'. Removing only known meta tags (using the 'og_type_mt' array as
1130 + * a reference) protects internal meta tags that may be used later by WpssoHead->extract_head_info().
1131 + *
1132 + * The 'og_content_map' array is also checked for Schema values that need to be swapped for simpler Open Graph meta
1133 + * tag values.
1134 + */
1135 + public function sanitize_mt_array( array $mt_og ) {
1136 +
1137 + /*
1138 + * Array of meta tags to allow, reject, and map.
1139 + */
1140 + static $og_allow = array();
1141 + static $og_reject = array();
1142 + static $content_map = array();
1143 + static $local_recursion = 0;
1144 +
1145 + if ( $local_recursion > 32 ) { // Just in case.
1146 +
1147 + return $mt_og;
1148 + }
1149 +
1150 + if ( 0 === $local_recursion ) { // Define the static variables once.
1151 +
1152 + /*
1153 + * The og:type is only needed when first run, to define the allow, reject, and map arrays.
1154 + */
1155 + if ( empty( $mt_og[ 'og:type' ] ) ) {
1156 +
1157 + if ( $this->p->debug->enabled ) {
1158 +
1159 + $this->p->debug->log( 'exiting early: og:type is empty and required' );
1160 + }
1161 +
1162 + return $mt_og;
1163 + }
1164 +
1165 + if ( $this->p->debug->enabled ) {
1166 +
1167 + $this->p->debug->log( 'setting og allow, reject, and content map arrays' );
1168 + }
1169 +
1170 + $og_type = $mt_og[ 'og:type' ];
1171 +
1172 + /*
1173 + * An array of Open Graph types, their meta tags, and their associated metadata keys.
1174 + */
1175 + foreach ( $this->p->cf[ 'head' ][ 'og_type_mt' ] as $type_id => $og_type_mt_md ) {
1176 +
1177 + /*
1178 + * An array of meta tags and their associated metadata keys.
1179 + */
1180 + foreach ( $og_type_mt_md as $mt_name => $md_key ) {
1181 +
1182 + if ( $og_type === $type_id ) {
1183 +
1184 + $og_allow[ $mt_name ] = true; // Mark meta tag as allowed.
1185 +
1186 + /*
1187 + * If we have a content map for the meta tag, save the content mapping array.
1188 + *
1189 + * 'product:availability' => array(
1190 + * 'https://schema.org/BackOrder' => 'available for order',
1191 + * 'https://schema.org/Discontinued' => 'discontinued',
1192 + * 'https://schema.org/InStock' => 'in stock',
1193 + * 'https://schema.org/InStoreOnly' => 'in stock',
1194 + * 'https://schema.org/LimitedAvailability' => 'in stock',
1195 + * 'https://schema.org/OnlineOnly' => 'in stock',
1196 + * 'https://schema.org/OutOfStock' => 'out of stock',
1197 + * 'https://schema.org/PreOrder' => 'available for order',
1198 + * 'https://schema.org/PreSale' => 'available for order',
1199 + * 'https://schema.org/SoldOut' => 'out of stock',
1200 + * ),
1201 + */
1202 + if ( ! empty( $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ] ) ) {
1203 +
1204 + /*
1205 + * Save the content mapping array.
1206 + */
1207 + $content_map[ $mt_name ] = $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ];
1208 + }
1209 +
1210 + } else $og_reject[ $mt_name ] = true; // Mark meta tag as disallowed.
1211 + }
1212 + }
1213 +
1214 + foreach ( array( 'product:offers', 'product:variants' ) as $mt_name ) {
1215 +
1216 + if ( ! empty( $mt_og[ $mt_name ] ) && is_array( $mt_og[ $mt_name ] ) ) {
1217 +
1218 + foreach ( $mt_og[ $mt_name ] as $num => &$mt_single ) { // Allow changes to the variation array.
1219 +
1220 + unset( $mt_single[ 'product:brand' ] ); // Allow only a single brand value (in the parent product).
1221 +
1222 + foreach ( $mt_single as $mt_single_name => $mt_single_value ) {
1223 +
1224 + /*
1225 + * Avoid duplicate values (like prices) between the main product and its variations.
1226 + */
1227 + if ( isset( $mt_og[ $mt_single_name ] ) && $mt_og[ $mt_single_name ] === $mt_single_value ) {
1228 +
1229 + /*
1230 + * Duplicate prices and values are removed first. Check if
1231 + * the currency or units meta tag needs to be removed as
1232 + * well (ie. if the price or value does not exist).
1233 + */
1234 + if ( true === $this->have_currency_units_value( $mt_og, $mt_single_name ) ) {
1235 +
1236 + if ( $this->p->debug->enabled ) {
1237 +
1238 + $this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' kept' );
1239 + }
1240 +
1241 + continue;
1242 + }
1243 +
1244 + if ( $this->p->debug->enabled ) {
1245 +
1246 + $this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' removed' );
1247 + }
1248 +
1249 + unset( $mt_og[ $mt_single_name ] );
1250 + }
1251 + }
1252 + }
1253 + }
1254 + }
1255 + }
1256 +
1257 + /*
1258 + * Check the meta tag names and their values.
1259 + */
1260 + foreach ( $mt_og as $key => $val ) {
1261 +
1262 + if ( ! empty( $og_allow[ $key ] ) ) { // Meta tag is allowed - check it's value.
1263 +
1264 + if ( $this->p->debug->enabled ) {
1265 +
1266 + $this->p->debug->log( 'allowed meta tag ' . $key );
1267 + }
1268 +
1269 + /*
1270 + * If we have a matching value in the content map, then assign the mapped value.
1271 + *
1272 + * 'https://schema.org/BackOrder' to 'available for order' for example.
1273 + */
1274 + if ( isset( $content_map[ $key ][ $val ] ) ) {
1275 +
1276 + if ( $this->p->debug->enabled ) {
1277 +
1278 + $this->p->debug->log( 'mapping meta tag ' . $key . ' = ' . $content_map[ $key ][ $val ] );
1279 + }
1280 +
1281 + $mt_og[ $key ] = $content_map[ $key ][ $val ];
1282 +
1283 + /*
1284 + * If this is a currency or units meta tag without a price or value, then set the meta tag to null.
1285 + */
1286 + } elseif ( false === $this->have_currency_units_value( $mt_og, $key ) ) {
1287 +
1288 + if ( $this->p->debug->enabled ) {
1289 +
1290 + $this->p->debug->log( 'setting meta tag ' . $key . ' = null' );
1291 + }
1292 +
1293 + $mt_og[ $key ] = null;
1294 + }
1295 +
1296 + } elseif ( ! empty( $og_reject[ $key ] ) ) {
1297 +
1298 + if ( $this->p->debug->enabled ) {
1299 +
1300 + $this->p->debug->log( 'rejected meta tag ' . $key );
1301 + }
1302 +
1303 + unset( $mt_og[ $key ] );
1304 +
1305 + } elseif ( is_array( $val ) ) {
1306 +
1307 + if ( $this->p->debug->enabled ) {
1308 +
1309 + if ( is_string( $key ) ) {
1310 +
1311 + $this->p->debug->log( 'sanitizing meta tag ' . $key . ' array' );
1312 + }
1313 + }
1314 +
1315 + $local_recursion++;
1316 +
1317 + $mt_og[ $key ] = $this->sanitize_mt_array( $val );
1318 +
1319 + $local_recursion--;
1320 + }
1321 + }
1322 +
1323 + return $mt_og;
1324 + }
1325 +
1326 + /*
1327 + * Add post/term/user meta data to the Open Graph meta tags.
1328 + */
1329 + public function add_data_og_type_md( array &$mt_og, $type_id, array $md_opts ) { // Pass by reference is OK.
1330 +
1331 + if ( $this->p->debug->enabled ) {
1332 +
1333 + $this->p->debug->mark();
1334 + }
1335 +
1336 + /*
1337 + * An array of Open Graph types, their meta tags, and their associated metadata keys.
1338 + */
1339 + if ( empty( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) { // Just in case.
1340 +
1341 + return;
1342 + }
1343 +
1344 + $og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ];
1345 +
1346 + if ( $this->p->debug->enabled ) {
1347 +
1348 + $this->p->debug->log( 'loading og_type_mt array for type id ' . $type_id );
1349 + }
1350 +
1351 + $md_keys_multi = WpssoConfig::get_md_keys_multi(); // Uses a local cache.
1352 +
1353 + foreach ( $og_type_mt_md as $mt_name => $md_key_pre ) {
1354 +
1355 + if ( empty( $md_key_pre ) ) { // Most of the metadata keys are empty.
1356 +
1357 + continue;
1358 +
1359 + } elseif ( false !== strpos( $mt_name, ':units' ) ) { // Define units when we define the value.
1360 +
1361 + continue;
1362 + }
1363 +
1364 + $is_multi = empty( $md_keys_multi[ $md_key_pre ] ) ? false : true;
1365 + $multi_keys = $is_multi ? array_keys( SucomUtil::preg_grep_keys( '/^' . $md_key_pre . '_[0-9]+$/', $md_opts ) ) : array( $md_key_pre );
1366 +
1367 + foreach ( $multi_keys as $md_key ) {
1368 +
1369 + $values = array();
1370 +
1371 + /*
1372 + * Use custom value(s) if available. Returns true or false.
1373 + */
1374 + if ( ! $this->have_og_type_md_values( $values, $type_id, $md_opts, $mt_name, $md_key ) ) {
1375 +
1376 + $def_md_key = $this->get_def_md_key( $md_key );
1377 +
1378 + if ( isset( $mt_og[ $mt_name ] ) ) { // Meta tag exists and is not null.
1379 +
1380 + if ( $this->p->debug->enabled ) {
1381 +
1382 + $this->p->debug->log( $mt_name . ' value kept = ' . $mt_og[ $mt_name ] );
1383 + }
1384 +
1385 + } elseif ( ! empty( $def_md_key ) ) { // Add default value from the plugin settings.
1386 +
1387 + if ( $this->p->debug->enabled ) {
1388 +
1389 + $this->p->debug->log( $mt_name . ' from options = ' . $this->p->options[ $def_md_key ] );
1390 + }
1391 +
1392 + $values[ $mt_name ] = $this->p->options[ $def_md_key ];
1393 +
1394 + } else { // Add missing meta tag with null value.
1395 +
1396 + if ( $this->p->debug->enabled ) {
1397 +
1398 + $this->p->debug->log( $mt_name . ' = null' );
1399 + }
1400 +
1401 + $values[ $mt_name ] = null; // Use null so isset() returns false.
1402 + }
1403 + }
1404 +
1405 + foreach ( $values as $mt_name => $val ) { // Can be an empty array.
1406 +
1407 + if ( 'none' === $val ) {
1408 +
1409 + if ( $this->p->debug->enabled ) {
1410 +
1411 + $this->p->debug->log( 'setting ' . $mt_name . ' = null for value none' );
1412 + }
1413 +
1414 + $mt_og[ $mt_name ] = null;
1415 +
1416 + } elseif ( $is_multi ) {
1417 +
1418 + $mt_og[ $mt_name ][] = $val;
1419 +
1420 + } else $mt_og[ $mt_name ] = $val;
1421 + }
1422 + }
1423 + }
1424 + }
1425 +
1426 + /*
1427 + * Check for a default value from the settings.
1428 + *
1429 + * Open Graph defaults have an 'og_def_' prefix, except for 'product_currency'.
1430 + *
1431 + * Schema defaults have a 'schema_def_' prefix.
1432 + */
1433 + public function get_def_md_key( $md_key ) {
1434 +
1435 + $def_md_key = null;
1436 +
1437 + if ( 'product_currency' === $md_key ) {
1438 +
1439 + if ( isset( $this->p->options[ 'og_def_currency' ] ) ) { // Just in case.
1440 +
1441 + $def_md_key = 'og_def_currency';
1442 + }
1443 +
1444 + } elseif ( isset( $this->p->options[ 'og_def_' . $md_key ] ) ) {
1445 +
1446 + $def_md_key = 'og_def_' . $md_key;
1447 +
1448 + } elseif ( isset( $this->p->options[ 'schema_def_' . $md_key ] ) ) {
1449 +
1450 + $def_md_key = 'schema_def_' . $md_key;
1451 + }
1452 +
1453 + return $def_md_key;
1454 + }
1455 +
1456 + /*
1457 + * If we have a GTIN number, try to improve the assigned property name.
1458 + *
1459 + * Pass $mt_og by reference to modify the array directly.
1460 + *
1461 + * A similar method exists as WpssoSchema::check_prop_value_gtin().
1462 + */
1463 + public static function check_mt_value_gtin( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK.
1464 +
1465 + $wpsso =& Wpsso::get_instance();
1466 +
1467 + if ( $wpsso->debug->enabled ) {
1468 +
1469 + $wpsso->debug->log( 'checking ' . $mt_pre . ' gtin value' );
1470 + }
1471 +
1472 + if ( isset( $mt_og[ $mt_pre . ':gtin' ] ) ) {
1473 +
1474 + /*
1475 + * The value may come from a custom field, so trim it, just in case.
1476 + */
1477 + $mt_og[ $mt_pre . ':gtin' ] = preg_replace( '/\s+/', '', $mt_og[ $mt_pre . ':gtin' ] );
1478 +
1479 + $gtin_len = strlen( $mt_og[ $mt_pre . ':gtin' ] );
1480 +
1481 + switch ( $gtin_len ) {
1482 +
1483 + case 13:
1484 +
1485 + if ( empty( $mt_og[ $mt_pre . ':ean' ] ) ) {
1486 +
1487 + $mt_og[ $mt_pre . ':ean' ] = $mt_og[ $mt_pre . ':gtin' ];
1488 + }
1489 +
1490 + break;
1491 +
1492 + case 12:
1493 +
1494 + if ( empty( $mt_og[ $mt_pre . ':upc' ] ) ) {
1495 +
1496 + $mt_og[ $mt_pre . ':upc' ] = $mt_og[ $mt_pre . ':gtin' ];
1497 + }
1498 +
1499 + break;
1500 + }
1501 + }
1502 + }
1503 +
1504 + public static function check_mt_value_price( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK.
1505 +
1506 + $wpsso =& Wpsso::get_instance();
1507 +
1508 + if ( $wpsso->debug->enabled ) {
1509 +
1510 + $wpsso->debug->log( 'checking ' . $mt_pre . ' price value' );
1511 + }
1512 +
1513 + $def_currency_key = $mt_pre . ':price:currency';
1514 + $def_currency = empty( $mt_og[ $def_currency_key ] ) ? $wpsso->options[ 'og_def_currency' ] : $mt_og[ $def_currency_key ];
1515 + $min_adv_price_key = $mt_pre . ':min_advert_price:amount';
1516 + $min_adv_price = empty( $mt_og[ $min_adv_price_key ] ) ? 0 : $mt_og[ $min_adv_price_key ];
1517 +
1518 + foreach ( array( 'min_advert_price', 'original_price', 'pretax_price', 'price', 'sale_price', 'shipping_cost' ) as $price_name ) {
1519 +
1520 + $amount_key = $mt_pre . ':' . $price_name . ':amount';
1521 + $currency_key = $mt_pre . ':' . $price_name . ':currency';
1522 +
1523 + if ( isset( $mt_og[ $amount_key ] ) ) {
1524 +
1525 + if ( is_numeric( $mt_og[ $amount_key ] ) ) { // Allow for price of 0.
1526 +
1527 + if ( $min_adv_price && $mt_og[ $amount_key ] < $min_adv_price ) {
1528 +
1529 + $mt_og[ $mt_pre . ':price:type' ] = 'https://schema.org/MinimumAdvertisedPrice';
1530 +
1531 + $mt_og[ $amount_key ] = $min_adv_price;
1532 + }
1533 +
1534 + if ( empty( $mt_og[ $currency_key ] ) ) {
1535 +
1536 + $mt_og[ $currency_key ] = $def_currency;
1537 + }
1538 +
1539 + } else {
1540 +
1541 + if ( ! empty( $mt_og[ $amount_key ] ) ) { // Non-empty string, array, etc.
1542 +
1543 + if ( $wpsso->debug->enabled ) {
1544 +
1545 + $wpsso->debug->log( 'invalid ' . $amount_key . ' value = ' . print_r( $mt_og[ $amount_key ], true ) );
1546 + }
1547 + }
1548 +
1549 + $mt_og[ $amount_key ] = null;
1550 + $mt_og[ $currency_key ] = null;
1551 + }
1552 + }
1553 + }
1554 + }
1555 +
1556 + public static function check_mt_value_energy_efficiency( &$mt_og, $mt_pre = 'product' ) { // Pass by reference is OK.
1557 +
1558 + $wpsso =& Wpsso::get_instance();
1559 +
1560 + if ( $wpsso->debug->enabled ) {
1561 +
1562 + $wpsso->debug->log( 'checking ' . $mt_pre . ' energy efficiency value' );
1563 + }
1564 +
1565 + if ( empty( $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) || 'none' === $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) {
1566 +
1567 + $mt_og[ $mt_pre . ':energy_efficiency:value' ] = null;
1568 + $mt_og[ $mt_pre . ':energy_efficiency:min_value' ] = null;
1569 + $mt_og[ $mt_pre . ':energy_efficiency:max_value' ] = null;
1570 + }
1571 + }
1572 +
1573 + private function have_currency_units_value( $mt_og, $mt_key ) {
1574 +
1575 + if ( preg_match( '/^(.*:)(currency|units)$/', $mt_key, $matches ) ) {
1576 +
1577 + switch ( $matches[ 2 ] ) {
1578 +
1579 + case 'currency': $mt_value_key = $matches[ 1 ] . 'amount'; break;
1580 + case 'units': $mt_value_key = $matches[ 1 ] . 'value'; break;
1581 + default: $mt_value_key = null; break;
1582 + }
1583 +
1584 + if ( $mt_value_key ) {
1585 +
1586 + return isset( $mt_og[ $mt_value_key ] ) && '' !== $mt_og[ $mt_value_key ] ? true : false;
1587 + }
1588 + }
1589 +
1590 + return null;
1591 + }
1592 +
1593 + /*
1594 + * Use custom value(s) if available. Returns true or false.
1595 + */
1596 + private function have_og_type_md_values( array &$values, $type_id, array $md_opts, $mt_name, $md_key ) { // Pass by reference is OK.
1597 +
1598 + if ( empty( $md_key ) || ! isset( $md_opts[ $md_key ] ) || '' === $md_opts[ $md_key ] ) {
1599 +
1600 + return false;
1601 + }
1602 +
1603 + /*
1604 + * Check for meta tags that require a unit value and may need to be converted.
1605 + */
1606 + if ( false !== strpos( $mt_name, ':value' ) && preg_match( '/^(.*):value$/', $mt_name, $mt_match ) ) {
1607 +
1608 + if ( $this->p->debug->enabled ) {
1609 +
1610 + $this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] );
1611 + }
1612 +
1613 + $values[ $mt_name ] = $md_opts[ $md_key ];
1614 +
1615 + /*
1616 + * Check if the value meta tag needs a units meta tag.
1617 + */
1618 + $mt_units_name = $mt_match[ 1 ] . ':units';
1619 +
1620 + /*
1621 + * An array of Open Graph types, their meta tags, and their associated metadata keys.
1622 + */
1623 + $og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ];
1624 +
1625 + if ( isset( $og_type_mt_md[ $mt_units_name ] ) ) { // Value needs a units meta tag.
1626 +
1627 + $md_units_key = $og_type_mt_md[ $mt_units_name ]; // An empty string or metadata options key.
1628 +
1629 + $values[ $mt_units_name ] = $unit_text = WpssoSchema::get_unit_text( $md_key );
1630 +
1631 + if ( ! empty( $md_opts[ $md_units_key ] ) ) { // Custom unit text found.
1632 +
1633 + if ( $this->p->debug->enabled ) {
1634 +
1635 + $this->p->debug->log( $mt_units_name . ' from metadata = ' . $md_opts[ $md_units_key ] );
1636 + }
1637 +
1638 + $values[ $mt_units_name ] = $md_opts[ $md_units_key ];
1639 + }
1640 +
1641 + /*
1642 + * Since WPSSO Core v14.0.0.
1643 + *
1644 + * Check if we need to convert the value.
1645 + */
1646 + if ( $values[ $mt_units_name ] !== $unit_text ) {
1647 +
1648 + $unit_value = WpssoUtilUnits::get_convert( $values[ $mt_name ], $unit_text, $values[ $mt_units_name ] );
1649 +
1650 + if ( $this->p->debug->enabled ) {
1651 +
1652 + $this->p->debug->log( 'converted ' . $values[ $mt_name ] . ' ' . $values[ $mt_units_name ] .
1653 + ' to ' . $unit_value . ' ' . $unit_text );
1654 + }
1655 +
1656 + $values[ $mt_name ] = $unit_value;
1657 + $values[ $mt_units_name ] = $unit_text;
1658 + }
1659 + }
1660 +
1661 + return true;
1662 + }
1663 +
1664 + /*
1665 + * If the value does not need to be converted, then add the custom metadata value as-is.
1666 + */
1667 + if ( $this->p->debug->enabled ) {
1668 +
1669 + $this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] );
1670 + }
1671 +
1672 + $values[ $mt_name ] = $md_opts[ $md_key ];
1673 +
1674 + return true;
1675 + }
1676 +
1677 + /*
1678 + * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and the Schema type,
1679 + * check for a possible hard-coded Open Graph type.
1680 + */
1681 + private function maybe_update_post_og_type( array $md_opts, $post_id, array $mod ) {
1682 +
1683 + if ( $this->p->debug->enabled ) {
1684 +
1685 + $this->p->debug->mark();
1686 + }
1687 +
1688 + if ( empty( $md_opts[ 'og_type:disabled' ] ) ) { // Just in case.
1689 +
1690 + /*
1691 + * Check if the post type matches a pre-defined Open Graph type.
1692 + *
1693 + * For example, a post type of 'organization' would return 'website' for the Open Graph type.
1694 + *
1695 + * Returns false or an Open Graph type string.
1696 + */
1697 + if ( $og_type = $this->p->post->get_post_type_og_type( $mod ) ) {
1698 +
1699 + $md_opts[ 'og_type' ] = $og_type;
1700 +
1701 + $md_opts[ 'og_type:disabled' ] = true;
1702 +
1703 + } else {
1704 +
1705 + /*
1706 + * Use the saved Schema type or get the default Schema type.
1707 + */
1708 + if ( isset( $md_opts[ 'schema_type' ] ) ) {
1709 +
1710 + $type_id = $md_opts[ 'schema_type' ];
1711 +
1712 + } else $type_id = $this->p->schema->get_mod_schema_type_id( $mod, $use_md_opts = false );
1713 +
1714 + /*
1715 + * Check if the Schema type matches a pre-defined Open Graph type.
1716 + */
1717 + if ( $og_type = $this->p->schema->get_schema_type_og_type( $type_id ) ) {
1718 +
1719 + $md_opts[ 'og_type' ] = $og_type;
1720 +
1721 + $md_opts[ 'og_type:disabled' ] = true;
1722 + }
1723 + }
1724 + }
1725 +
1726 + return $md_opts;
1727 + }
1728 + }
1729 +}