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
wpsso / lib / schema-single.php

schema-single.php in WPSSO Core – Complete Schema Markup and Meta Tags 22.7.0, at lib/schema-single.php

3,399 lines 105.6 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( 'WpssoSchemaSingle' ) ) {
19
20 class WpssoSchemaSingle {
21
22 /*
23 * See WpssoJsonTypeBook->filter_json_data_https_schema_org_book().
24 */
25 public static function add_book_data( &$json_data, array $mod, $book_id = false, $def_type_id = 'book', $list_el = false ) {
26
27 $wpsso =& Wpsso::get_instance();
28
29 if ( $wpsso->debug->enabled ) {
30
31 $wpsso->debug->mark();
32 }
33
34 /*
35 * Maybe get options from integration modules.
36 */
37 $book_opts = apply_filters( 'wpsso_get_book_options', false, $mod, $book_id );
38
39 if ( ! empty( $book_opts ) ) {
40
41 if ( $wpsso->debug->enabled ) {
42
43 $wpsso->debug->log_arr( 'get_book_options', $book_opts );
44 }
45 }
46
47 /*
48 * Add metadata defaults and custom values to the $book_opts array.
49 *
50 * Automatically renames 'schema_book_*' options from the Document SSO metabox to 'book_*'.
51 */
52 WpssoSchema::add_type_opts_md_pad( $book_opts, $mod, array( 'book' => 'schema_book' ) );
53
54 if ( empty( $book_opts ) ) {
55
56 if ( $wpsso->debug->enabled ) {
57
58 $wpsso->debug->log( 'exiting early: no book options' );
59 }
60
61 return 0; // Return count of books added.
62 }
63
64 /*
65 * Add metadata defaults and custom values to the $book_opts array.
66 *
67 * Automatically renames 'schema_book_*' options from the Document SSO metabox to 'book_*'.
68 */
69 WpssoSchema::add_type_opts_md_pad( $book_opts, $mod, array( 'book' => 'schema_book' ) );
70
71 /*
72 * If not adding a list element, get the existing schema type url (if one exists).
73 */
74 list( $type_id, $type_url ) = self::get_data_types( $json_data, $book_opts, $opt_key = 'book_type', $def_type_id, $list_el );
75
76 /*
77 * Maybe remove values related to the WordPress post object.
78 */
79 if ( $wpsso->debug->enabled ) {
80
81 $wpsso->debug->log( 'unsetting json_data author, contributor, dateCreated, datePublished, dateModified' );
82 }
83
84 unset( $json_data[ 'author' ] );
85 unset( $json_data[ 'contributor' ] );
86 unset( $json_data[ 'dateCreated' ] );
87 unset( $json_data[ 'datePublished' ] );
88 unset( $json_data[ 'dateModified' ] );
89
90 /*
91 * Begin schema book markup creation.
92 */
93 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
94
95 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $book_opts, array( // Returns the number of properties added.
96 'isbn' => 'book_isbn',
97 'bookFormat' => 'book_format',
98 'bookEdition' => 'book_edition',
99 'numberOfPages' => 'book_pages',
100 ) );
101
102 /*
103 * At a minimum, we need an author type and name.
104 *
105 * The author type value should be either 'organization' or 'person'.
106 */
107 if ( WpssoSchema::is_valid_key( $book_opts, 'book_author_type' ) ) { // Not null, an empty string, or 'none'.
108
109 if ( WpssoSchema::is_valid_key( $book_opts, 'book_author_name' ) ) { // Not null, an empty string, or 'none'.
110
111 /*
112 * Calling add_data_itemprop_from_assoc() with non-existant property sets it to null by default.
113 */
114 if ( WpssoSchema::add_data_itemprop_from_assoc( $json_ret[ 'author' ], $book_opts, array( // Returns the number of properties added.
115 'name' => 'book_author_name',
116 ) ) ) {
117 $author_type_url = $wpsso->schema->get_schema_type_url( $book_opts[ 'book_author_type' ] );
118
119 WpssoSchema::add_schema_type_context( $author_type_url, $json_ret[ 'author' ] );
120
121 if ( WpssoSchema::is_valid_key( $book_opts, 'book_author_url' ) ) { // Not null, an empty string, or 'none'.
122
123 $json_ret[ 'author' ][ 'sameAs' ][] = SucomUtil::esc_url_encode( $book_opts[ 'book_author_url' ] );
124 }
125
126 }
127
128 } elseif ( $wpsso->debug->enabled ) {
129
130 $wpsso->debug->log( 'no book author name' );
131 }
132
133 } elseif ( $wpsso->debug->enabled ) {
134
135 $wpsso->debug->log( 'no book author type' );
136 }
137
138 /*
139 * Book Published Date, Time, Timezone.
140 *
141 * Add the creative work published date, if one is available.
142 */
143 if ( $date = WpssoSchema::get_opts_date_iso( $book_opts, 'book_pub' ) ) {
144
145 $json_ret[ 'datePublished' ] = $date;
146 }
147
148 /*
149 * Book Created Date, Time, Timezone.
150 *
151 * Add the creative work created date, if one is available.
152 */
153 if ( $date = WpssoSchema::get_opts_date_iso( $book_opts, 'book_created' ) ) {
154
155 $json_ret[ 'dateCreated' ] = $date;
156 }
157
158 /*
159 * Add book offers.
160 */
161 WpssoSchema::add_type_data_offers( $json_ret, $mod, $book_opts, $book_id, 'book' );
162
163 /*
164 * Add or replace the json data.
165 */
166 self::add_or_replace_data( $json_data, $json_ret, $list_el );
167
168 return 1; // Return count of books added.
169 }
170
171 /**
172 * See WpssoSchema::add_comments_data().
173 * See WpssoSchemaSingle::add_comment_reply_data().
174 */
175 public static function add_comment_data( &$json_data, array $post_mod, $comment_id, $list_el = true ) {
176
177 $wpsso =& Wpsso::get_instance();
178
179 if ( empty( $comment_id ) ) { // Just in case.
180
181 return 0; // Return count of comments added.
182 }
183
184 $comment_mod = $wpsso->comment->get_mod( $comment_id );
185
186 if ( ! $comment_mod[ 'is_comment' ] || ! $comment_mod[ 'id' ] ) {
187
188 return 0; // Return count of comments added.
189 }
190
191 /*
192 * If not adding a list element, get the existing schema type url (if one exists).
193 */
194 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id = 'comment', $list_el );
195
196 /*
197 * Begin schema comment markup creation.
198 */
199 $json_ret = WpssoSchema::get_schema_type_context( $type_url, array(
200 'url' => $wpsso->util->get_canonical_url( $comment_mod ),
201 'name' => $wpsso->page->get_title( $comment_mod, $md_key = 'schema_title', $max_len = 'schema_title' ),
202 'description' => $wpsso->page->get_description( $comment_mod, $md_key = 'schema_desc', $max_len = 'schema_desc' ),
203 'text' => $wpsso->page->get_text( $comment_mod, $md_key = 'schema_text', $max_len = 'schema_text' ),
204 'dateCreated' => $comment_mod[ 'comment_time' ],
205 'author' => WpssoSchema::get_schema_type_context( 'https://schema.org/Person', array(
206 'url' => $comment_mod[ 'comment_author_url' ],
207 'name' => $comment_mod[ 'comment_author_name' ],
208 ) ),
209 ) );
210
211 /*
212 * Property:
213 * image as https://schema.org/ImageObject
214 * video as https://schema.org/VideoObject
215 */
216 if ( $wpsso->debug->enabled ) {
217
218 $wpsso->debug->log( 'adding image and video properties for comment' );
219 }
220
221 $mt_comment = $wpsso->og->get_array( $comment_mod, $size_names = 'schema', $md_pre = array( 'schema', 'og') );
222
223 WpssoSchema::add_media_data( $json_ret, $comment_mod, $mt_comment, $size_names = 'schema', $add_video = true );
224
225 /*
226 * Add post comment replies.
227 */
228 $replies_added = self::add_comment_reply_data( $json_ret[ 'comment' ], $post_mod, $comment_id );
229
230 /*
231 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $comment_id values.
232 */
233 WpssoSchema::update_data_id( $json_ret, array( $type_id, $comment_id ) );
234
235 /*
236 * Add or replace the json data.
237 */
238 self::add_or_replace_data( $json_data, $json_ret, $list_el );
239
240 return 1; // Return count of comments added.
241 }
242
243 /*
244 * See WpssoSchemaSingle::add_comment_data().
245 * See WpssoJsonPropReview->filter_json_data_https_schema_org_thing().
246 */
247 public static function add_comment_reply_data( &$json_data, array $post_mod, $comment_id ) {
248
249 $wpsso =& Wpsso::get_instance();
250
251 $replies_added = 0;
252
253 $replies = get_comments( array(
254 'post_id' => $post_mod[ 'id' ],
255 'status' => 'approve',
256 'parent' => $comment_id, // Get only the replies for this comment.
257 'order' => 'DESC',
258 'number' => get_option( 'page_comments' ), // Limit the number of comments.
259 ) );
260
261 if ( is_array( $replies ) ) {
262
263 foreach( $replies as $num => $comment_obj ) {
264
265 $comments_added = self::add_comment_data( $json_data, $post_mod, $comment_obj->comment_ID, $comment_list_el = true );
266
267 if ( $comments_added ) {
268
269 $replies_added += $comments_added;
270 }
271 }
272 }
273
274 return $replies_added; // Return count of replies added.
275 }
276
277 public static function add_contact_data( &$json_data, array $mod, $contact_id, $list_el = false ) {
278
279 $wpsso =& Wpsso::get_instance();
280
281 if ( $wpsso->debug->enabled ) {
282
283 $wpsso->debug->mark();
284 }
285
286 /*
287 * Maybe get options from integration modules.
288 *
289 * Returned options can change depending on the locale, but the option key names should NOT be localized.
290 */
291 $contact_opts = apply_filters( 'wpsso_get_contact_options', false, $mod, $contact_id );
292
293 if ( ! empty( $contact_opts ) ) {
294
295 if ( $wpsso->debug->enabled ) {
296
297 $wpsso->debug->log_arr( 'get_contact_options', $contact_opts );
298 }
299 }
300
301 /*
302 * If not adding a list element, get the existing schema type url (if one exists).
303 */
304 list( $type_id, $type_url ) = self::get_data_types( $json_data, $contact_opts, $opt_key = 'contact_schema_type',
305 $def_type_id = 'contact.point', $list_el );
306
307 /*
308 * Begin schema contact markup creation.
309 */
310 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
311
312 /*
313 * Add schema properties from the contact options.
314 */
315 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $contact_opts, array( // Returns the number of properties added.
316 'url' => 'contact_url',
317 'name' => 'contact_name',
318 'alternateName' => 'contact_name_alt',
319 'description' => 'contact_desc',
320 ) );
321
322 /*
323 * Area served property.
324 */
325 WpssoSchema::add_type_data_areaserved( $json_ret, $mod, $contact_opts, $contact_id, 'contact_service' );
326
327 /*
328 * Property:
329 * hoursAvailable as https://schema.org/OpeningHoursSpecification
330 */
331 if ( $opening_hours_spec = self::get_opening_hours_data( $contact_opts, $opt_prefix = 'contact' ) ) {
332
333 $json_ret[ 'hoursAvailable' ] = $opening_hours_spec;
334 }
335
336 /*
337 * Schema PostalAddress type properties.
338 */
339 if ( $wpsso->schema->is_schema_type_child( $type_id, 'postal.address' ) ) {
340
341 if ( ! empty( $contact_opts[ 'contact_street_address' ] ) ) { // Just in case.
342
343 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $contact_opts, array( // Returns the number of properties added.
344 'streetAddress' => 'contact_street_address',
345 'postOfficeBoxNumber' => 'contact_po_box_number',
346 'addressLocality' => 'contact_city',
347 'addressRegion' => 'contact_region',
348 'postalCode' => 'contact_postal_code',
349 'addressCountry' => 'contact_country', // Alpha2 country code.
350 ) );
351 }
352 }
353
354 /*
355 * SameAs property (ie. Knowledge Graph).
356 */
357 WpssoSchema::add_type_data_sameas( $json_ret, $mod, $contact_opts, $contact_id, 'contact' );
358
359 /*
360 * Update the @id string with the $json_ret[ 'url' ], $type_id and $contact_id.
361 */
362 WpssoSchema::update_data_id( $json_ret, array( $type_id, $contact_id ) );
363
364 /*
365 * Add or replace the json data.
366 */
367 self::add_or_replace_data( $json_data, $json_ret, $list_el );
368
369 return 1; // Return count of contacts added.
370 }
371
372 /*
373 * See WpssoJsonTypeEvent->filter_json_data_https_schema_org_event().
374 */
375 public static function add_event_data( &$json_data, array $mod, $event_id = false, $list_el = false ) {
376
377 $wpsso =& Wpsso::get_instance();
378
379 if ( $wpsso->debug->enabled ) {
380
381 $wpsso->debug->mark();
382 }
383
384 /*
385 * Maybe get options from integration modules.
386 *
387 * Returned options can change depending on the locale, but the option key names should NOT be localized.
388 */
389 $event_opts = apply_filters( 'wpsso_get_event_options', false, $mod, $event_id );
390
391 if ( ! empty( $event_opts ) ) {
392
393 if ( $wpsso->debug->enabled ) {
394
395 $wpsso->debug->log_arr( 'get_event_options', $event_opts );
396 }
397 }
398
399 /*
400 * Add metadata defaults and custom values to the $event_opts array.
401 *
402 * Automatically renames 'schema_event_*' options from the Document SSO metabox to 'event_*'.
403 */
404 WpssoSchema::add_type_opts_md_pad( $event_opts, $mod, array( 'event' => 'schema_event' ) );
405
406 if ( empty( $event_opts ) ) {
407
408 if ( $wpsso->debug->enabled ) {
409
410 $wpsso->debug->log( 'exiting early: no event options' );
411 }
412
413 return 0; // Return count of events added.
414 }
415
416 /*
417 * Add ISO formatted date options.
418 */
419 if ( $wpsso->debug->enabled ) {
420
421 $wpsso->debug->log( 'checking for custom event start/end date and time' );
422 }
423
424 /*
425 * Get dates from the meta data options and add ISO formatted dates to the array (passed by reference).
426 *
427 * {event option name} => {meta data option name}.
428 */
429 WpssoSchema::add_mod_opts_date_iso( $mod, $event_opts, array(
430 'event_start_date' => 'schema_event_start', // Prefix for date, time, timezone, iso.
431 'event_end_date' => 'schema_event_end', // Prefix for date, time, timezone, iso.
432 'event_previous_date' => 'schema_event_previous', // Prefix for date, time, timezone, iso.
433 'event_offers_start_date' => 'schema_event_offers_start', // Prefix for date, time, timezone, iso.
434 'event_offers_end_date' => 'schema_event_offers_end', // Prefix for date, time, timezone, iso.
435 ) );
436
437 /*
438 * If not adding a list element, get the existing schema type url (if one exists).
439 */
440 list( $type_id, $type_url ) = self::get_data_types( $json_data, $event_opts, $opt_key = 'event_type', $def_type_id = 'event', $list_el );
441
442 /*
443 * Begin schema event markup creation.
444 */
445 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
446
447 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $event_opts, array(
448 'inLanguage' => 'event_lang',
449 'eventAttendanceMode' => 'event_attendance',
450 'eventStatus' => 'event_status',
451 'previousStartDate' => 'event_previous_date_iso',
452 'startDate' => 'event_start_date_iso',
453 'endDate' => 'event_end_date_iso',
454 ) );
455
456 /*
457 * Events with a previous start date must have rescheduled as their status.
458 *
459 * Rescheduled events, without a previous start date, is an invalid combination.
460 */
461 if ( ! empty( $json_ret[ 'previousStartDate' ] ) ) {
462
463 $json_ret[ 'eventStatus' ] = 'https://schema.org/EventRescheduled';
464
465 } elseif ( isset( $json_ret[ 'eventStatus' ] ) && 'https://schema.org/EventRescheduled' === $json_ret[ 'eventStatus' ] ) {
466
467 $json_ret[ 'eventStatus' ] = 'https://schema.org/EventScheduled';
468 }
469
470 /*
471 * Add place, organization, and person data.
472 *
473 * Use $opt_pre => $prop_name association as the property name may be repeated (ie. non-unique).
474 */
475 foreach ( array(
476 'event_online_url' => 'location',
477 'event_location_id' => 'location',
478 'event_performer_org_id' => 'performer',
479 'event_performer_person_id' => 'performer',
480 'event_organizer_org_id' => 'organizer',
481 'event_organizer_person_id' => 'organizer',
482 'event_fund_org_id' => 'funder',
483 'event_fund_person_id' => 'funder',
484 ) as $opt_pre => $prop_name ) {
485
486 foreach ( SucomUtil::get_multi_values( $event_opts, $opt_pre ) as $id ) { // Uses is_valid_option_value().
487
488 switch ( $opt_pre ) {
489
490 case 'event_online_url':
491
492 $json_ret[ $prop_name ][] = WpssoSchema::get_schema_type_context( 'https://schema.org/VirtualLocation',
493 array( 'url' => $event_opts[ $opt_pre ] ) );
494
495 break;
496
497 case 'event_location_id':
498
499 self::add_place_data( $json_ret[ $prop_name ], $mod, $id, $place_list_el = true );
500
501 break;
502
503 case 'event_organizer_org_id':
504 case 'event_performer_org_id':
505
506 self::add_organization_data( $json_ret[ $prop_name ], $mod, $id, 'org_logo_url', $org_list_el = true );
507
508 break;
509
510 case 'event_organizer_person_id':
511 case 'event_performer_person_id':
512
513 self::add_person_data( $json_ret[ $prop_name ], $mod, $id, $person_list_el = true );
514
515 break;
516 }
517 }
518 }
519
520 /*
521 * Add event offers.
522 */
523 WpssoSchema::add_type_data_offers( $json_ret, $mod, $event_opts, $event_id, 'event' );
524
525 /*
526 * Filter the single event data.
527 */
528 if ( $wpsso->debug->enabled ) {
529
530 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_event"' );
531 }
532
533 $json_ret = apply_filters( 'wpsso_json_data_single_event', $json_ret, $mod, $event_id );
534
535 /*
536 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $event_id values.
537 */
538 WpssoSchema::update_data_id( $json_ret, array( $type_id, $event_id ) );
539
540 /*
541 * Add or replace the json data.
542 */
543 self::add_or_replace_data( $json_data, $json_ret, $list_el );
544
545 return 1; // Return count of events added.
546 }
547
548 /*
549 * This method converts an 'og:image' array into Schema ImageObject data.
550 *
551 * Pass a single dimension image array in $mt_single.
552 */
553 public static function add_image_data_mt( &$json_data, array $mt_single, $mt_pre = 'og:image', $list_el = true ) {
554
555 $wpsso =& Wpsso::get_instance();
556
557 if ( $wpsso->debug->enabled ) {
558
559 $wpsso->debug->mark();
560 }
561
562 if ( empty( $mt_single ) || ! is_array( $mt_single ) ) {
563
564 if ( $wpsso->debug->enabled ) {
565
566 $wpsso->debug->log( 'exiting early: options array is empty or not an array' );
567 }
568
569 return 0; // Return count of images added.
570 }
571
572 $image_url = SucomUtil::get_first_mt_media_url( $mt_single, $mt_pre );
573
574 if ( empty( $image_url ) ) {
575
576 if ( $wpsso->debug->enabled ) {
577
578 $wpsso->debug->log( 'exiting early: ' . $mt_pre . ' URL values are empty' );
579 }
580
581 return 0; // Return count of images added.
582 }
583
584 /*
585 * If not adding a list element, get the existing schema type url (if one exists).
586 */
587 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id = 'image.object', $list_el );
588
589 /*
590 * Begin schema image markup creation.
591 */
592 $json_ret = WpssoSchema::get_schema_type_context( $type_url, array(
593 'url' => SucomUtil::esc_url_encode( $image_url ),
594 ) );
595
596 /*
597 * Maybe add an 'identifier' value with the size name and image ID.
598 */
599 if ( ! empty( $mt_single[ $mt_pre . ':id' ] ) ) {
600
601 $json_ret[ 'identifier' ] = $mt_single[ $mt_pre . ':id' ];
602
603 if ( ! empty( $mt_single[ $mt_pre . ':size_name' ] ) ) {
604
605 $json_ret[ 'identifier' ] .= '-' . $mt_single[ $mt_pre . ':size_name' ];
606 }
607 }
608
609 /*
610 * If we have an ID, and it's numeric, check the WordPress Media Library for a title and description.
611 */
612 if ( ! empty( $mt_single[ $mt_pre . ':id' ] ) && is_numeric( $mt_single[ $mt_pre . ':id' ] ) ) {
613
614 $post_id = $mt_single[ $mt_pre . ':id' ];
615 $mod = $wpsso->post->get_mod( $post_id );
616
617 /*
618 * Get the image title.
619 */
620 $json_ret[ 'name' ] = $wpsso->page->get_title( $mod, $md_key = 'schema_title', $max_len = 'schema_title' );
621
622 /*
623 * Get the image alternate title, if one has been defined in the custom post meta.
624 */
625 $json_ret[ 'alternateName' ] = $wpsso->page->get_title( $mod, $md_key = 'schema_title_alt', $max_len = 'schema_title_alt' );
626
627 if ( $json_ret[ 'name' ] === $json_ret[ 'alternateName' ] ) { // Prevent duplicate values.
628
629 unset( $json_ret[ 'alternateName' ] );
630 }
631
632 /*
633 * Use the image "Alternative Text" for the 'alternativeHeadline' property.
634 */
635 $json_ret[ 'alternativeHeadline' ] = get_metadata( 'post', $mod[ 'id' ], '_wp_attachment_image_alt', true );
636
637 if ( $json_ret[ 'name' ] === $json_ret[ 'alternativeHeadline' ] ) { // Prevent duplicate values.
638
639 unset( $json_ret[ 'alternativeHeadline' ] );
640 }
641
642 /*
643 * Get the image caption (aka excerpt of the post object).
644 */
645 $json_ret[ 'caption' ] = $wpsso->page->get_the_excerpt( $mod );
646 $json_ret[ 'description' ] = $wpsso->page->get_description( $mod, $md_key = 'schema_desc', $max_len = 'schema_desc' );
647
648 /*
649 * Set the 'encodingFormat' property to the image mime type.
650 */
651 $json_ret[ 'encodingFormat' ] = $mod[ 'post_mime_type' ];
652
653 /*
654 * Set the 'uploadDate' property to the image attachment publish time.
655 */
656 $json_ret[ 'uploadDate' ] = $mod[ 'post_time' ];
657 }
658
659 if ( ! empty( $mt_single[ $mt_pre . ':alt' ] ) ) {
660
661 $json_ret[ 'alternativeHeadline' ] = $mt_single[ $mt_pre . ':alt' ];
662 }
663
664 if ( ! empty( $mt_single[ $mt_pre . ':tag' ] ) ) {
665
666 if ( is_array( $mt_single[ $mt_pre . ':tag' ] ) ) {
667
668 $json_ret[ 'keywords' ] = implode( $glue = ', ', $mt_single[ $mt_pre . ':tag' ] );
669
670 } else {
671
672 $json_ret[ 'keywords' ] = $mt_single[ $mt_pre . ':tag' ];
673 }
674 }
675
676 /*
677 * Add width and height as QuantitativeValue.
678 */
679 WpssoSchema::add_data_unit_from_assoc( $json_ret, $mt_single, array(
680 'width_px' => $mt_pre . ':width',
681 'height_px' => $mt_pre . ':height',
682 ) );
683
684 /*
685 * Update the @id string with the $json_ret[ 'url' ] and $type_id.
686 */
687 WpssoSchema::update_data_id( $json_ret, $type_id );
688
689 /*
690 * Add or replace the json data.
691 */
692 self::add_or_replace_data( $json_data, $json_ret, $list_el );
693
694 return 1; // Return count of images added.
695 }
696
697 public static function add_job_data( &$json_data, array $mod, $job_id = false, $list_el = false ) {
698
699 $wpsso =& Wpsso::get_instance();
700
701 if ( $wpsso->debug->enabled ) {
702
703 $wpsso->debug->mark();
704 }
705
706 /*
707 * Maybe get options from integration modules.
708 */
709 $job_opts = apply_filters( 'wpsso_get_job_options', false, $mod, $job_id );
710
711 if ( ! empty( $job_opts ) ) {
712
713 if ( $wpsso->debug->enabled ) {
714
715 $wpsso->debug->log_arr( 'get_job_options', $job_opts );
716 }
717 }
718
719 /*
720 * Add metadata defaults and custom values to the $job_opts array.
721 *
722 * Automatically renames 'schema_job_*' options from the Document SSO metabox to 'job_*'.
723 */
724 WpssoSchema::add_type_opts_md_pad( $job_opts, $mod, array( 'job' => 'schema_job' ) );
725
726 if ( empty( $job_opts ) ) {
727
728 if ( $wpsso->debug->enabled ) {
729
730 $wpsso->debug->log( 'exiting early: no job options' );
731 }
732
733 return 0; // Return count of jobs added.
734 }
735
736 if ( empty( $job_opts[ 'job_title' ] ) ) {
737
738 $job_opts[ 'job_title' ] = $wpsso->page->get_title( $mod, $md_key = 'schema_job_title', $max_len = 'schema_title' );
739 }
740
741 /*
742 * Add ISO formatted date options.
743 */
744 if ( $wpsso->debug->enabled ) {
745
746 $wpsso->debug->log( 'checking for custom job expire date and time' );
747 }
748
749 /*
750 * Get dates from the meta data options and add ISO formatted dates to the array (passed by reference).
751 *
752 * {job option} => {meta data option} (ie. the option name from the document SSO metabox).
753 */
754 WpssoSchema::add_mod_opts_date_iso( $mod, $job_opts, $opts_md_pre = array(
755 'job_expire' => 'schema_job_expire', // Prefix for date, time, timezone, iso.
756 ) );
757
758 /*
759 * If not adding a list element, get the existing schema type url (if one exists).
760 */
761 list( $type_id, $type_url ) = self::get_data_types( $json_data, $job_opts, $opt_key = 'job_type', $def_type_id = 'job.posting', $list_el );
762
763 /*
764 * Begin schema job markup creation.
765 */
766 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
767
768 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $job_opts, array(
769 'title' => 'job_title',
770 'validThrough' => 'job_expire_iso',
771 'jobLocationType' => 'job_location_type',
772 ) );
773
774 if ( isset( $job_opts[ 'job_salary' ] ) && is_numeric( $job_opts[ 'job_salary' ] ) ) { // Allow for 0.
775
776 $json_ret[ 'baseSalary' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/MonetaryAmount' );
777
778 WpssoSchema::add_data_itemprop_from_assoc( $json_ret[ 'baseSalary' ], $job_opts, array(
779 'currency' => 'job_salary_currency',
780 ) );
781
782 $json_ret[ 'baseSalary' ][ 'value' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/QuantitativeValue' );
783
784 WpssoSchema::add_data_itemprop_from_assoc( $json_ret[ 'baseSalary' ][ 'value' ], $job_opts, array(
785 'value' => 'job_salary',
786 'unitText' => 'job_salary_period',
787 ) );
788 }
789
790 /*
791 * Allow for a preformatted employment types array.
792 */
793 if ( ! empty( $job_opts[ 'job_empl_types' ] ) && is_array( $job_opts[ 'job_empl_types' ] ) ) {
794
795 $json_ret[ 'employmentType' ] = $job_opts[ 'job_empl_types' ];
796 }
797
798 /*
799 * Add employment type options (value must be non-empty).
800 */
801 foreach ( $wpsso->cf[ 'form' ][ 'employment_type' ] as $empl_type => $label ) {
802
803 if ( 'none' !== $empl_type && ! empty( $job_opts[ 'job_empl_type_' . $empl_type ] ) ) {
804
805 $json_ret[ 'employmentType' ][] = $empl_type;
806 }
807 }
808
809 /*
810 * Add place, organization, and person data.
811 *
812 * Use $opt_pre => $prop_name association as the property name may be repeated (ie. non-unique).
813 */
814 foreach ( array(
815 'job_hiring_org_id' => 'hiringOrganization',
816 'job_location_id' => 'jobLocation',
817 ) as $opt_pre => $prop_name ) {
818
819 foreach ( SucomUtil::get_multi_values( $job_opts, $opt_pre ) as $id ) { // Uses is_valid_option_value().
820
821 switch ( $opt_pre ) {
822
823 case 'job_hiring_org_id':
824
825 self::add_organization_data( $json_ret[ $prop_name ], $mod, $id, 'org_logo_url', $org_list_el = true );
826
827 break;
828
829 case 'job_location_id':
830
831 self::add_place_data( $json_ret[ $prop_name ], $mod, $id, $place_list_el = true );
832
833 break;
834 }
835 }
836 }
837
838 /*
839 * Filter the single job data.
840 */
841 if ( $wpsso->debug->enabled ) {
842
843 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_job"' );
844 }
845
846 $json_ret = apply_filters( 'wpsso_json_data_single_job', $json_ret, $mod, $job_id );
847
848 /*
849 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $job_id values.
850 */
851 WpssoSchema::update_data_id( $json_ret, array( $type_id, $job_id ) );
852
853 /*
854 * Add or replace the json data.
855 */
856 self::add_or_replace_data( $json_data, $json_ret, $list_el );
857
858 return 1; // Return count of jobs added.
859 }
860
861 /*
862 * See https://developers.google.com/search/docs/appearance/structured-data/return-policy.
863 */
864 public static function add_merchant_return_policy_data( &$json_data, array $mod, $mrp_id, $list_el = false ) {
865
866 $wpsso =& Wpsso::get_instance();
867
868 if ( $wpsso->debug->enabled ) {
869
870 $wpsso->debug->mark();
871 }
872
873 /*
874 * Maybe get options from integration modules.
875 *
876 * Returned options can change depending on the locale, but the option key names should NOT be localized.
877 */
878 $mrp_opts = apply_filters( 'wpsso_get_merchant_return_policy_options', false, $mod, $mrp_id );
879
880 if ( empty( $mrp_opts ) ) {
881
882 if ( $wpsso->debug->enabled ) {
883
884 $wpsso->debug->log( 'exiting early: no return policy options' );
885 }
886
887 return 0; // Return count of return policies added.
888
889 } elseif ( $wpsso->debug->enabled ) {
890
891 $wpsso->debug->log_arr( 'get_merchant_return_policy_options', $mrp_opts );
892 }
893
894 list( $type_id, $type_url ) = array( 'merchant.return.policy', 'https://schema.org/MerchantReturnPolicy' );
895
896 $countries = SucomUtil::preg_grep_keys( '/^mrp_country_(.*)$/',
897 $mrp_opts, $invert = false, $replace = '$1' );
898 $countries = array_keys( array_filter( $countries ) ); // Remove unchecked options.
899
900 $methods = SucomUtil::preg_grep_keys( '/^mrp_method_https_schema_org_(.*)$/',
901 $mrp_opts, $invert = false, $replace = 'https://schema.org/$1' );
902 $methods = array_keys( array_filter( $methods ) ); // Remove unchecked options.
903
904 $refund_types = SucomUtil::preg_grep_keys( '/^mrp_refund_type_https_schema_org_(.*)$/',
905 $mrp_opts, $invert = false, $replace = 'https://schema.org/$1' );
906 $refund_types = array_keys( array_filter( $refund_types ) ); // Remove unchecked options.
907
908 $item_conditions = SucomUtil::preg_grep_keys( '/^mrp_item_condition_https_schema_org_(.*)$/',
909 $mrp_opts, $invert = false, $replace = 'https://schema.org/$1' );
910 $item_conditions = array_keys( array_filter( $item_conditions ) ); // Remove unchecked options.
911
912 /*
913 * Begin schema merchant return policy markup creation.
914 */
915 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
916
917 /*
918 * Add schema properties from the return policy options.
919 */
920 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $mrp_opts, array(
921 'name' => 'mrp_name',
922 'description' => 'mrp_desc',
923 'returnPolicyCategory' => 'mrp_category',
924 ) );
925
926 $json_ret[ 'refundType' ] = $refund_types;
927
928 $json_ret[ 'itemCondition' ] = $item_conditions;
929
930 if ( isset( $mrp_opts[ 'mrp_category' ] ) ) {
931
932 switch ( basename( $mrp_opts[ 'mrp_category' ] ) ) {
933
934 case 'MerchantReturnFiniteReturnWindow':
935
936 if ( isset( $mrp_opts[ 'mrp_return_days' ] ) ) {
937
938 $json_ret[ 'merchantReturnDays' ] = $mrp_opts[ 'mrp_return_days' ];
939 }
940
941 // No break.
942
943 case 'MerchantReturnUnlimitedWindow':
944
945 $json_ret[ 'returnMethod' ] = $methods;
946
947 break;
948 }
949
950 } elseif ( $wpsso->debug->enabled ) {
951
952 $wpsso->debug->log( 'no return method' );
953 }
954
955 /*
956 * The method by which the consumer can obtain a return shipping label for a product.
957 */
958 if ( ! empty( $mrp_opts[ 'mrp_return_label_source' ] ) ) {
959
960 $json_ret[ 'returnLabelSource' ] = $mrp_opts[ 'mrp_return_label_source' ];
961
962 } elseif ( $wpsso->debug->enabled ) {
963
964 $wpsso->debug->log( 'no return label source' );
965 }
966
967 /*
968 * The type of return fees. This property is only required if there's no cost to return the product. If you
969 * use this property, you must set the value to https://schema.org/FreeReturn (other return fee types
970 * aren't supported; if there are fees, use the returnShippingFeesAmount property instead).
971 */
972 if ( ! empty( $mrp_opts[ 'mrp_return_fees' ] ) ) {
973
974 $json_ret[ 'returnFees' ] = $mrp_opts[ 'mrp_return_fees' ];
975
976 if ( 'https://schema.org/ReturnShippingFees' === $mrp_opts[ 'mrp_return_fees' ] ) {
977
978 if ( empty( $mrp_opts[ 'mrp_shipping_amount' ] ) ) { // Amount is 0.
979
980 $json_ret[ 'returnFees' ] = 'https://schema.org/FreeReturn';
981
982 } elseif ( ! empty( $mrp_opts[ 'mrp_shipping_currency' ] ) ) { // Just in case.
983
984 $json_ret[ 'returnShippingFeesAmount' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/MonetaryAmount', array(
985 'value' => $mrp_opts[ 'mrp_shipping_amount' ],
986 'currency' => $mrp_opts[ 'mrp_shipping_currency' ],
987 ) );
988 }
989 }
990
991 } elseif ( $wpsso->debug->enabled ) {
992
993 $wpsso->debug->log( 'no return fees' );
994 }
995
996 /*
997 * The restocking fee charged to the consumer when returning a product. Specify a value of type Number to
998 * charge a percentage of the price paid by the consumer or use MonetaryAmount to charge a fixed amount.
999 */
1000 if ( ! empty( $mrp_opts[ 'mrp_restocking_amount' ] ) &&
1001 ! empty( $mrp_opts[ 'mrp_restocking_currency' ] ) ) {
1002
1003 $json_ret[ 'restockingFee' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/MonetaryAmount', array(
1004 'value' => $mrp_opts[ 'mrp_restocking_amount' ],
1005 'currency' => $mrp_opts[ 'mrp_restocking_currency' ],
1006 ) );
1007
1008 } elseif ( ! empty( $mrp_opts[ 'mrp_restocking_pct' ] ) ) {
1009
1010 $json_ret[ 'restockingFee' ] = $mrp_opts[ 'mrp_restocking_pct' ];
1011
1012 } elseif ( $wpsso->debug->enabled ) {
1013
1014 $wpsso->debug->log( 'no restocking amount or percentage' );
1015 }
1016
1017 /*
1018 * Add applicableCountry property.
1019 */
1020 if ( ! empty( $countries ) ) {
1021
1022 $json_ret[ 'applicableCountry' ] = $countries;
1023
1024 } elseif ( $wpsso->debug->enabled ) {
1025
1026 $wpsso->debug->log( 'no applicable countries' );
1027 }
1028
1029 /*
1030 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $mrp_id values.
1031 */
1032 WpssoSchema::update_data_id( $json_ret, array( $type_id, $mrp_id ), '/' );
1033
1034 /*
1035 * Add or replace the json data.
1036 */
1037 self::add_or_replace_data( $json_data, $json_ret, $list_el );
1038
1039 return 1; // Return count of return policies added.
1040 }
1041
1042 public static function add_organization_data( &$json_data, array $mod, $org_id, $org_logo_key = 'org_logo_url', $list_el = false, $called_by = false ) {
1043
1044 $wpsso =& Wpsso::get_instance();
1045
1046 if ( $wpsso->debug->enabled ) {
1047
1048 $wpsso->debug->log_args( array(
1049 'org_id' => $org_id,
1050 'org_logo_key' => $org_logo_key,
1051 'list_el' => $list_el,
1052 'called_by' => $called_by,
1053 ) );
1054 }
1055
1056 if ( ! SucomUtil::is_valid_option_value( $org_id ) ) {
1057
1058 if ( ! empty( $mod[ 'obj' ] ) ) { // Just in case.
1059
1060 if ( $wpsso->debug->enabled ) {
1061
1062 $wpsso->debug->log( 'getting schema_organization_id from metadata' );
1063 }
1064
1065 $org_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'schema_organization_id', $filter_opts = true, $merge_defs = true );
1066 }
1067
1068 if ( ! SucomUtil::is_valid_option_value( $org_id ) ) {
1069
1070 if ( ! empty( $mod[ 'is_home' ] ) ) { // Home page (static or blog archive).
1071
1072 if ( $wpsso->debug->enabled ) {
1073
1074 $wpsso->debug->log( 'using site organization for home' );
1075 }
1076
1077 $org_id = 'site';
1078 }
1079 }
1080 }
1081
1082 /*
1083 * Maybe get options from integration modules.
1084 *
1085 * Returned options can change depending on the locale, but the option key names should NOT be localized.
1086 *
1087 * Example 'org_banner_url' is a valid option key, but 'org_banner_url#fr_FR' is not.
1088 */
1089 if ( 'site' === $org_id ) {
1090
1091 if ( $wpsso->debug->enabled ) {
1092
1093 $wpsso->debug->log( 'getting site organization options array' );
1094 }
1095
1096 $org_opts = WpssoSchema::get_site_organization( $mod );
1097
1098 } else {
1099
1100 if ( $wpsso->debug->enabled ) {
1101
1102 $wpsso->debug->log( 'applying filters "wpsso_get_organization_options" for org_id = ' . $org_id );
1103 }
1104
1105 $org_opts = apply_filters( 'wpsso_get_organization_options', false, $mod, $org_id );
1106 }
1107
1108 if ( empty( $org_opts ) ) {
1109
1110 if ( $wpsso->debug->enabled ) {
1111
1112 $wpsso->debug->log( 'exiting early: no org options' );
1113 }
1114
1115 return 0; // Return count of organizations added.
1116
1117 } elseif ( $wpsso->debug->enabled ) {
1118
1119 $wpsso->debug->log_arr( 'get_organization_options', $org_opts );
1120 }
1121
1122 /*
1123 * If not adding a list element, get the existing schema type url (if one exists).
1124 */
1125 list( $type_id, $type_url ) = self::get_data_types( $json_data, $org_opts, $opt_key = 'org_schema_type', $def_type_id = 'organization', $list_el );
1126
1127 /*
1128 * Begin schema organization markup creation. Preserve the @id value if one is available.
1129 */
1130 $json_ret = WpssoSchema::get_schema_type_context( $type_url, SucomUtil::preg_grep_keys( '/^@/', $json_data ) );
1131
1132 if ( $wpsso->debug->enabled ) {
1133
1134 $wpsso->debug->log_arr( 'get_schema_type_context', $json_ret );
1135 }
1136
1137 /*
1138 * Set the reference values for admin notices.
1139 */
1140 if ( is_admin() ) {
1141
1142 $canonical_url = $wpsso->util->get_canonical_url( $mod );
1143
1144 $wpsso->util->maybe_set_ref( $canonical_url, $mod,
1145 ( 'site' === $org_id ? __( 'adding schema organization', 'wpsso' ) :
1146 sprintf( __( 'adding schema organization ID "%s"', 'wpsso' ), $org_id ) ) );
1147 }
1148
1149 /*
1150 * Add schema properties from the organization options.
1151 */
1152 SucomUtil::add_multi_values( $org_opts, 'org_award', 'org_awards' );
1153
1154 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $org_opts, array(
1155 'url' => 'org_url',
1156 'name' => 'org_name',
1157 'alternateName' => 'org_name_alt',
1158 'description' => 'org_desc',
1159 'email' => 'org_email',
1160 'telephone' => 'org_phone',
1161 'publishingPrinciples' => 'org_pub_principles_url', // Publishing Principles URL.
1162 'correctionsPolicy' => 'org_corrections_policy_url', // Corrections Policy URL.
1163 'diversityPolicy' => 'org_diversity_policy_url', // Diversity Policy URL.
1164 'ethicsPolicy' => 'org_ethics_policy_url', // Ethics Policy URL.
1165 'verificationFactCheckingPolicy' => 'org_fact_check_policy_url', // Fact Checking Policy URL.
1166 'actionableFeedbackPolicy' => 'org_feedback_policy_url', // Feedback Policy URL.
1167 'award' => 'org_awards', // Organization Awards.
1168 ) );
1169
1170 /*
1171 * See https://schema.org/contactPoint.
1172 */
1173 foreach ( SucomUtil::get_multi_values( $org_opts, 'org_contact_id' ) as $contact_id ) {
1174
1175 self::add_contact_data( $json_ret[ 'contactPoint' ], $mod, $contact_id, $contact_list_el = true );
1176 }
1177
1178 /*
1179 * Area served property.
1180 */
1181 WpssoSchema::add_type_data_areaserved( $json_ret, $mod, $org_opts, $org_id, 'org_service' );
1182
1183 /*
1184 * See https://schema.org/hasOfferCatalog.
1185 */
1186 WpssoSchema::add_offer_catalogs_data( $json_ret, $mod, $org_opts, $opt_pre = 'org_offer_catalog', $prop_name = 'hasOfferCatalog' );
1187
1188 /*
1189 * Schema NewsMediaOrganization type properties.
1190 */
1191 if ( $wpsso->schema->is_schema_type_child( $type_id, 'news.media.organization' ) ) {
1192
1193 if ( $wpsso->debug->enabled ) {
1194
1195 $wpsso->debug->log( 'adding news media properties' );
1196 }
1197
1198 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $org_opts, array(
1199 'masthead' => 'org_masthead_url', // Masthead Page URL.
1200 'missionCoveragePrioritiesPolicy' => 'org_coverage_policy_url', // Coverage Priorities Policy URL.
1201 'noBylinesPolicy' => 'org_no_bylines_policy_url', // No Bylines Policy URL.
1202 'unnamedSourcesPolicy' => 'org_sources_policy_url', // Unnamed Sources Policy URL.
1203 ) );
1204 }
1205
1206 /*
1207 * Organization images.
1208 */
1209 if ( ! empty( $org_opts[ 'org_img_id' ] ) || ! empty( $org_opts[ 'org_img_url' ] ) ) {
1210
1211 /*
1212 * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
1213 */
1214 $mt_images = $wpsso->media->get_mt_opts_images( $org_opts, $size_names = 'schema', $img_prefix = 'org_img' );
1215
1216 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $mt_images );
1217 }
1218
1219 if ( ! empty( $org_opts[ 'org_images' ] ) ) {
1220
1221 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $org_opts[ 'org_images' ] );
1222 }
1223
1224 /*
1225 * Google requires at least one image so fallback to using the Organization logo.
1226 */
1227 if ( empty( $json_ret[ 'image' ] ) ) {
1228
1229 $org_image_key = 'org_logo_url';
1230
1231 if ( $wpsso->debug->enabled ) {
1232
1233 $wpsso->debug->log( 'adding image from ' . $org_image_key . ' option' );
1234 }
1235
1236 if ( ! empty( $org_opts[ $org_image_key ] ) ) {
1237
1238 self::add_image_data_mt( $json_ret[ 'image' ], $org_opts, $org_image_key );
1239 }
1240 }
1241
1242 /*
1243 * Organization logo.
1244 *
1245 * $org_logo_key can be empty, 'org_logo_url', or 'org_banner_url' for Articles.
1246 */
1247 if ( ! empty( $org_logo_key ) ) {
1248
1249 if ( $wpsso->debug->enabled ) {
1250
1251 $wpsso->debug->log( 'adding logo from ' . $org_logo_key . ' option' );
1252 }
1253
1254 if ( ! empty( $org_opts[ $org_logo_key ] ) ) {
1255
1256 self::add_image_data_mt( $json_ret[ 'logo' ], $org_opts, $org_logo_key, $image_list_el = false );
1257 }
1258
1259 // translators: %1$s is the organization name, %2$s the Schema type URL.
1260 $logo_missing_msg = __( 'An organization logo image is missing and required for the "%1$s" organization Schema %2$s markup.', 'wpsso' );
1261
1262 // translators: %1$s is the organization name, %2$s the Schema type URL.
1263 $banner_missing_msg = __( 'An organization banner image is missing and required for the "%1$s" organization Schema %2$s markup.', 'wpsso' );
1264
1265 // translators: %1$s is the organization name, %2$s is 'site' (translated) or organization ID.
1266 $org_settings_msg = __( 'Enter the missing image URL in the "%1$s" %2$s organization settings.', 'wpsso' );
1267
1268 if ( ! $mod[ 'is_post' ] || 'publish' === $mod[ 'post_status' ] ) {
1269
1270 if ( ! empty( $json_ret[ 'name' ] ) && empty( $json_ret[ 'logo' ] ) ) {
1271
1272 if ( $wpsso->debug->enabled ) {
1273
1274 $wpsso->debug->log( 'organization ' . $org_logo_key . ' image is missing and required' );
1275 }
1276
1277 /*
1278 * Add notice only if the admin notices have not already been shown.
1279 */
1280 if ( $wpsso->notice->is_admin_pre_notices() ) {
1281
1282 if ( 'org_logo_url' === $org_logo_key ) {
1283
1284 $notice_msg = sprintf( $logo_missing_msg, $json_ret[ 'name' ],
1285 '<a href="' . $type_url . '">'. $type_url . '</a>' );
1286
1287 } elseif ( 'org_banner_url' === $org_logo_key ) {
1288
1289 $notice_msg = sprintf( $banner_missing_msg, $json_ret[ 'name' ],
1290 '<a href="' . $type_url . '">'. $type_url . '</a>' );
1291
1292 } else $notice_msg = '';
1293
1294 if ( $notice_msg ) {
1295
1296 /*
1297 * Site organization.
1298 */
1299 if ( 'site' === $org_id ) {
1300
1301 $settings_page_url = $wpsso->util->get_admin_url( 'essential' );
1302 $org_id_transl = __( 'site', 'wpsso' );
1303
1304 $notice_msg .= ' ';
1305 $notice_msg .= '<a href="' . $settings_page_url . '">';
1306 $notice_msg .= sprintf( $org_settings_msg, $json_ret[ 'name' ], $org_id_transl );
1307 $notice_msg .= '</a>';
1308
1309 /*
1310 * WPSSO Organization and Place add-on organization ID.
1311 */
1312 } elseif ( 0 === strpos( $org_id, 'org-' ) && ! empty( $wpsso->avail[ 'p_ext' ][ 'opm' ] ) ) {
1313
1314 $post_id = substr( $org_id, 4 );
1315 $org_page_link = get_edit_post_link( $post_id );
1316 $org_id_transl = sprintf( __( 'ID "%s"', 'wpsso' ), $org_id );
1317
1318 $notice_msg .= ' ';
1319 $notice_msg .= $org_page_link ? '<a href="' . $org_page_link . '">' : '';
1320 $notice_msg .= sprintf( $org_settings_msg, $json_ret[ 'name' ], $org_id_transl );
1321 $notice_msg .= $org_page_link ? '</a>' : '';
1322
1323 } else {
1324
1325 $org_id_transl = sprintf( __( 'ID "%s"', 'wpsso' ), $org_id );
1326
1327 $notice_msg .= ' ';
1328 $notice_msg .= sprintf( $org_settings_msg, $json_ret[ 'name' ], $org_id_transl );
1329 }
1330
1331 $notice_key = $mod[ 'name' ] . '-' . $mod[ 'id' ] . '-notice-missing-schema-' . $org_logo_key;
1332
1333 $wpsso->notice->err( $notice_msg, null, $notice_key );
1334 }
1335 }
1336 }
1337 }
1338 }
1339
1340 /*
1341 * Location property.
1342 */
1343 if ( ! $wpsso->schema->is_schema_type_child( $type_id, 'place' ) ) { // Just in case.
1344
1345 if ( isset( $org_opts[ 'org_place_id' ] ) ) {
1346
1347 if ( SucomUtil::is_valid_option_value( $org_opts[ 'org_place_id' ] ) ) { // Not true, false, null, empty string, or 'none'.
1348
1349 self::add_place_data( $json_ret[ 'location' ], $mod, $org_opts[ 'org_place_id' ], $place_list_el = false );
1350 }
1351 }
1352 }
1353
1354 /*
1355 * SameAs property (ie. Knowledge Graph).
1356 */
1357 WpssoSchema::add_type_data_sameas( $json_ret, $mod, $org_opts, $org_id, 'org' );
1358
1359 /*
1360 * Filter the single organization data.
1361 */
1362 if ( $wpsso->debug->enabled ) {
1363
1364 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_organization"' );
1365 }
1366
1367 $json_ret = apply_filters( 'wpsso_json_data_single_organization', $json_ret, $mod, $org_id );
1368
1369 /*
1370 * Restore previous reference values for admin notices.
1371 */
1372 if ( is_admin() ) {
1373
1374 $wpsso->util->maybe_unset_ref( $canonical_url );
1375 }
1376
1377 /*
1378 * Update the @id string with the $json_ret[ 'url' ], $type_id, $org_id, and $org_logo_key values.
1379 */
1380 WpssoSchema::update_data_id( $json_ret, array( $type_id, $org_id, $org_logo_key ) );
1381
1382 /*
1383 * If this organization is also a sub-type of place, then add the schema place properties as well.
1384 *
1385 * Prevent recursion by making sure this method wasn't called by self::add_place_data().
1386 *
1387 * Add place adata and merge BEFORE calling add_or_replace_data() in case we're adding a list element.
1388 */
1389 if ( $wpsso->schema->is_schema_type_child( $type_id, 'place' ) ) {
1390
1391 if ( 'add_place_data' !== $called_by ) {
1392
1393 if ( $wpsso->debug->enabled ) {
1394
1395 $wpsso->debug->log( 'adding place data for org id "' . $org_id . '"' );
1396 }
1397
1398 self::add_place_data( $json_ret, $mod, $org_id, 'merge', __FUNCTION__ );
1399 }
1400 }
1401
1402 /*
1403 * Add or replace the json data.
1404 */
1405 self::add_or_replace_data( $json_data, $json_ret, $list_el );
1406
1407 unset( $json_ret );
1408
1409 if ( $wpsso->debug->enabled ) {
1410
1411 $wpsso->debug->log_arr( 'json_data', $json_data );
1412 }
1413
1414 return 1; // Return count of organizations added.
1415 }
1416
1417 /*
1418 * A $person_id argument is required.
1419 */
1420 public static function add_person_data( &$json_data, array $mod, $person_id, $list_el = true ) {
1421
1422 $wpsso =& Wpsso::get_instance();
1423
1424 if ( $wpsso->debug->enabled ) {
1425
1426 $wpsso->debug->mark();
1427 }
1428
1429 /*
1430 * Maybe get options from integration modules (example: WpssoIntegEventTheEventsCalendar).
1431 */
1432 $person_opts = apply_filters( 'wpsso_get_person_options', false, $mod, $person_id );
1433
1434 if ( ! empty( $person_opts ) ) {
1435
1436 if ( $wpsso->debug->enabled ) {
1437
1438 $wpsso->debug->log_arr( 'get_person_options', $person_opts );
1439 }
1440 }
1441
1442 /*
1443 * Fallback to using person data from the WordPress user profile.
1444 */
1445 $canonical_url = '';
1446
1447 if ( empty( $person_opts ) ) {
1448
1449 if ( empty( $person_id ) || 'none' === $person_id ) { // 0, an empty string, or a 'none' string.
1450
1451 if ( $wpsso->debug->enabled ) {
1452
1453 $wpsso->debug->log( 'exiting early: no person id' );
1454 }
1455
1456 return 0; // Return count of persons added.
1457 }
1458
1459 static $local_fifo = array();
1460
1461 if ( ! isset( $local_fifo[ $person_id ] ) ) {
1462
1463 /*
1464 * Maybe limit the number of array elements.
1465 */
1466 $local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );
1467
1468 $local_fifo[ $person_id ] = array();
1469 }
1470
1471 if ( ! isset( $local_fifo[ $person_id ][ 'person_opts' ] ) ) {
1472
1473 if ( $wpsso->debug->enabled ) {
1474
1475 $wpsso->debug->log( 'getting user module for person_id ' . $person_id );
1476 }
1477
1478 $user_mod = $wpsso->user->get_mod( $person_id );
1479
1480 $local_fifo[ $person_id ][ 'canonical_url' ] = $wpsso->util->get_canonical_url( $user_mod );
1481
1482 /*
1483 * Set the reference values for admin notices.
1484 */
1485 if ( is_admin() ) {
1486
1487 $wpsso->util->maybe_set_ref( $local_fifo[ $person_id ][ 'canonical_url' ], $user_mod, __( 'adding schema person', 'wpsso' ) );
1488 }
1489
1490 $user_description = $wpsso->page->get_description( $user_mod, $md_key = 'schema_desc', $max_len = 'schema_desc' );
1491 $user_images = $wpsso->media->get_all_images( $num = 1, $size_names = 'schema', $user_mod, $md_pre = array( 'schema', 'og' ) );
1492 $user_sameas = $wpsso->user->get_user_sameas( $person_id );
1493
1494 $local_fifo[ $person_id ][ 'person_opts' ] = array(
1495 'person_type' => 'person',
1496 'person_url' => $user_mod[ 'obj' ]->get_author_website( $person_id, 'url' ), // Returns a single URL string.
1497 'person_name' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'display_name' ),
1498 'person_first_name' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'first_name' ),
1499 'person_last_name' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'last_name' ),
1500 'person_job_title' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'job_title' ),
1501 'person_prefix' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'honorific_prefix' ),
1502 'person_suffix' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'honorific_suffix' ),
1503 'person_addl_name' => $user_mod[ 'obj' ]->get_author_meta( $person_id, 'additional_name' ),
1504 'person_desc' => $user_description,
1505 'person_images' => $user_images,
1506 'person_sameas' => $user_sameas,
1507 );
1508
1509 /*
1510 * Restore previous reference values for admin notices.
1511 */
1512 if ( is_admin() ) {
1513
1514 $wpsso->util->maybe_unset_ref( $local_fifo[ $person_id ][ 'canonical_url' ] );
1515 }
1516
1517 if ( $wpsso->debug->enabled ) {
1518
1519 $wpsso->debug->log_arr( 'local_cache_person_opts', $local_fifo[ $person_id ][ 'person_opts' ] );
1520 }
1521 }
1522
1523 $person_opts =& $local_fifo[ $person_id ][ 'person_opts' ];
1524 $canonical_url =& $local_fifo[ $person_id ][ 'canonical_url' ];
1525 }
1526
1527 /*
1528 * If not adding a list element, get the existing schema type url (if one exists).
1529 */
1530 list( $type_id, $type_url ) = self::get_data_types( $json_data, $person_opts, $opt_key = 'person_type', $def_type_id = 'person', $list_el );
1531
1532 /*
1533 * Begin schema person markup creation.
1534 */
1535 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
1536
1537 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $person_opts, array(
1538 'url' => 'person_url',
1539 'name' => 'person_name',
1540 'givenName' => 'person_first_name',
1541 'familyName' => 'person_last_name',
1542 'honorificPrefix' => 'person_prefix',
1543 'honorificSuffix' => 'person_suffix',
1544 'additionalName' => 'person_addl_name',
1545 'description' => 'person_desc',
1546 'jobTitle' => 'person_job_title',
1547 'email' => 'person_email',
1548 'telephone' => 'person_phone',
1549 ) );
1550
1551 /*
1552 * Person images.
1553 */
1554 if ( ! empty( $person_opts[ 'person_img_id' ] ) || ! empty( $person_opts[ 'person_img_url' ] ) ) {
1555
1556 /*
1557 * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
1558 */
1559 $mt_images = $wpsso->media->get_mt_opts_images( $person_opts, $size_names = 'schema', $img_prefix = 'person_img' );
1560
1561 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $mt_images );
1562
1563 }
1564
1565 if ( ! empty( $person_opts[ 'person_images' ] ) ) {
1566
1567 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $person_opts[ 'person_images' ] );
1568 }
1569
1570 /*
1571 * SameAs property (ie. Knowledge Graph).
1572 */
1573 WpssoSchema::add_type_data_sameas( $json_ret, $mod, $person_opts, $person_id, 'person' );
1574
1575 /*
1576 * Filter the single person data.
1577 */
1578 if ( $wpsso->debug->enabled ) {
1579
1580 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_person"' );
1581 }
1582
1583 $json_ret = apply_filters( 'wpsso_json_data_single_person', $json_ret, $mod, $person_id );
1584
1585 /*
1586 * Update the '@id' string with $canonical_url and $type_id.
1587 *
1588 * Encode the URL part of the '@id' string to hide the WordPress login username.
1589 */
1590 WpssoSchema::update_data_id( $json_ret, $type_id, $canonical_url, $hash_url = true );
1591
1592 /*
1593 * Add or replace the json data.
1594 */
1595 self::add_or_replace_data( $json_data, $json_ret, $list_el );
1596
1597 return 1; // Return count of persons added.
1598 }
1599
1600 /*
1601 * See WpssoSchemaSingle::add_event_data().
1602 * See WpssoSchemaSingle::add_job_data().
1603 * See WpssoSchemaSingle::add_organization_data().
1604 */
1605 public static function add_place_data( &$json_data, array $mod, $place_id, $list_el = false, $called_by = false ) {
1606
1607 $wpsso =& Wpsso::get_instance();
1608
1609 if ( $wpsso->debug->enabled ) {
1610
1611 $wpsso->debug->mark();
1612 }
1613
1614 if ( ! SucomUtil::is_valid_option_value( $place_id ) ) {
1615
1616 if ( ! empty( $mod[ 'obj' ] ) ) { // Just in case.
1617
1618 if ( $wpsso->debug->enabled ) {
1619
1620 $wpsso->debug->log( 'getting schema_place_id from metadata' );
1621 }
1622
1623 $place_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'schema_place_id', $filter_opts = true, $merge_defs = true );
1624 }
1625 }
1626
1627 /*
1628 * Maybe get options from integration modules.
1629 *
1630 * Returned options can change depending on the locale, but the option key names should not be localized.
1631 */
1632 if ( $wpsso->debug->enabled ) {
1633
1634 $wpsso->debug->log( 'applying filters "wpsso_get_place_options" for place_id = ' . $place_id );
1635 }
1636
1637 $place_opts = apply_filters( 'wpsso_get_place_options', false, $mod, $place_id );
1638
1639 if ( empty( $place_opts ) ) {
1640
1641 if ( $wpsso->debug->enabled ) {
1642
1643 $wpsso->debug->log( 'exiting early: no place options' );
1644 }
1645
1646 return 0; // Return count of places added.
1647
1648 } elseif ( $wpsso->debug->enabled ) {
1649
1650 $wpsso->debug->log_arr( 'get_place_options', $place_opts );
1651 }
1652
1653 /*
1654 * If not adding a list element, get the existing schema type url (if one exists).
1655 */
1656 list( $type_id, $type_url ) = self::get_data_types( $json_data, $place_opts, $opt_key = 'place_schema_type', $def_type_id = 'place', $list_el );
1657
1658 /*
1659 * Begin schema place markup creation. Preserve the @id value if one is available.
1660 */
1661 $json_ret = WpssoSchema::get_schema_type_context( $type_url, SucomUtil::preg_grep_keys( '/^@/', $json_data ) );
1662
1663 if ( $wpsso->debug->enabled ) {
1664
1665 $wpsso->debug->log_arr( 'get_schema_type_context', $json_ret );
1666 }
1667
1668 /*
1669 * If this place is also a sub-type of organization, then add the schema organization properties as well.
1670 *
1671 * Prevent recursion by making sure this method wasn't called by self::add_organization_data().
1672 */
1673 if ( $wpsso->schema->is_schema_type_child( $type_id, 'organization' ) ) {
1674
1675 if ( 'add_organization_data' !== $called_by ) {
1676
1677 if ( $wpsso->debug->enabled ) {
1678
1679 $wpsso->debug->log( 'adding organization data for place id "' . $place_id . '"' );
1680 }
1681
1682 self::add_organization_data( $json_data, $mod, $place_id, 'org_logo_url', 'merge', __FUNCTION__ );
1683 }
1684 }
1685
1686 /*
1687 * Set reference values for admin notices.
1688 */
1689 if ( is_admin() ) {
1690
1691 $canonical_url = $wpsso->util->get_canonical_url( $mod );
1692
1693 $wpsso->util->maybe_set_ref( $canonical_url, $mod, __( 'adding schema place', 'wpsso' ) );
1694 }
1695
1696 /*
1697 * Add schema properties from the place options.
1698 */
1699 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $place_opts, array(
1700 'url' => 'place_url',
1701 'name' => 'place_name',
1702 'alternateName' => 'place_name_alt',
1703 'description' => 'place_desc',
1704 'telephone' => 'place_phone',
1705 'faxNumber' => 'place_fax',
1706 ) );
1707
1708 /*
1709 * Property:
1710 * address as https://schema.org/PostalAddress
1711 */
1712 if ( ! empty( $place_opts[ 'place_street_address' ] ) ) {
1713
1714 $json_ret[ 'address' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/PostalAddress',
1715 WpssoSchema::get_data_itemprop_from_assoc( $place_opts, array(
1716 'name' => 'place_name',
1717 'streetAddress' => 'place_street_address',
1718 'postOfficeBoxNumber' => 'place_po_box_number',
1719 'addressLocality' => 'place_city',
1720 'addressRegion' => 'place_region',
1721 'postalCode' => 'place_postal_code',
1722 'addressCountry' => 'place_country', // Alpha2 country code.
1723 ) )
1724 );
1725 }
1726
1727 /*
1728 * Property:
1729 * geo as https://schema.org/GeoCoordinates
1730 */
1731 $geo = array();
1732
1733 if ( WpssoSchema::add_data_itemprop_from_assoc( $geo, $place_opts, array(
1734 'elevation' => 'place_altitude',
1735 'latitude' => 'place_latitude',
1736 'longitude' => 'place_longitude',
1737 ) ) ) {
1738
1739 $json_ret[ 'geo' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/GeoCoordinates', $geo );
1740 }
1741
1742 /*
1743 * Property:
1744 * openingHoursSpecification as https://schema.org/OpeningHoursSpecification
1745 */
1746 if ( $opening_hours_spec = self::get_opening_hours_data( $place_opts, $opt_prefix = 'place' ) ) {
1747
1748 $json_ret[ 'openingHoursSpecification' ] = $opening_hours_spec;
1749 }
1750
1751 /*
1752 * Schema LocalBusiness type properties.
1753 */
1754 if ( $wpsso->schema->is_schema_type_child( $type_id, 'local.business' ) ) {
1755
1756 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $place_opts, array(
1757 'currenciesAccepted' => 'place_currencies_accepted',
1758 'paymentAccepted' => 'place_payment_accepted',
1759 'priceRange' => 'place_price_range',
1760 ) );
1761 }
1762
1763 /*
1764 * Schema FoodEstablishment type properties.
1765 */
1766 if ( $wpsso->schema->is_schema_type_child( $type_id, 'food.establishment' ) ) {
1767
1768 foreach ( array(
1769 'acceptsReservations' => 'place_accept_res',
1770 'hasMenu' => 'place_menu_url',
1771 'servesCuisine' => 'place_cuisine',
1772 ) as $prop_name => $opt_key ) {
1773
1774 if ( 'place_accept_res' === $opt_key ) {
1775
1776 $json_ret[ $prop_name ] = empty( $place_opts[ $opt_key ] ) ? 'false' : 'true';
1777
1778 } elseif ( isset( $place_opts[ $opt_key ] ) ) {
1779
1780 $json_ret[ $prop_name ] = $place_opts[ $opt_key ];
1781 }
1782 }
1783
1784 if ( ! empty( $place_opts[ 'place_order_urls' ] ) ) {
1785
1786 foreach ( SucomUtil::explode_csv( $place_opts[ 'place_order_urls' ] ) as $order_url ) {
1787
1788 if ( empty( $order_url ) ) { // Just in case.
1789
1790 continue;
1791 }
1792
1793 $json_ret[ 'potentialAction' ][] = WpssoSchema::get_schema_type_context( 'https://schema.org/OrderAction',
1794 array( 'target' => $order_url ) );
1795 }
1796 }
1797 }
1798
1799 /*
1800 * Place images.
1801 */
1802 if ( ! empty( $place_opts[ 'place_img_id' ] ) || ! empty( $place_opts[ 'place_img_url' ] ) ) {
1803
1804 /*
1805 * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
1806 */
1807 $mt_images = $wpsso->media->get_mt_opts_images( $place_opts, $size_names = 'schema', $img_prefix = 'place_img' );
1808
1809 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $mt_images );
1810 }
1811
1812 if ( ! empty( $place_opts[ 'place_images' ] ) ) {
1813
1814 WpssoSchema::add_images_data_mt( $json_ret[ 'image' ], $place_opts[ 'place_images' ] );
1815 }
1816
1817 /*
1818 * SameAs property (ie. Knowledge Graph).
1819 */
1820 WpssoSchema::add_type_data_sameas( $json_ret, $mod, $place_opts, $place_id, 'place' );
1821
1822 /*
1823 * Filter the single place data.
1824 */
1825 if ( $wpsso->debug->enabled ) {
1826
1827 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_place"' );
1828 }
1829
1830 $json_ret = apply_filters( 'wpsso_json_data_single_place', $json_ret, $mod, $place_id );
1831
1832 /*
1833 * Restore previous reference values for admin notices.
1834 */
1835 if ( is_admin() ) {
1836
1837 $wpsso->util->maybe_unset_ref( $canonical_url );
1838 }
1839
1840 /*
1841 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $place_id values.
1842 */
1843 WpssoSchema::update_data_id( $json_ret, array( $type_id, $place_id ) );
1844
1845 /*
1846 * Add or replace the json data.
1847 */
1848 self::add_or_replace_data( $json_data, $json_ret, $list_el );
1849
1850 return 1; // Return count of places added.
1851 }
1852
1853 /*
1854 * See WpssoSchemaSingle::get_offer_data_mt().
1855 */
1856 public static function add_offer_data_mt( &$json_data, array $mod, array $mt_single, $def_type_id = 'offer', $list_el = true ) {
1857
1858 $wpsso =& Wpsso::get_instance();
1859
1860 if ( $wpsso->debug->enabled ) {
1861
1862 $wpsso->debug->mark();
1863
1864 /*
1865 * Log the post type and post ID. Example:
1866 *
1867 * adding offer data for product id = 4425
1868 * adding offer data for product_variation id = 4439
1869 */
1870 $wpsso->debug->log( 'adding offer data for ' . $mod[ 'post_type' ] . ' id = ' . $mod[ 'id' ] );
1871 }
1872
1873 /*
1874 * If not adding a list element, get the existing schema type url (if one exists).
1875 */
1876 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id, $list_el );
1877
1878 /*
1879 * Begin schema product markup creation.
1880 */
1881 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
1882
1883 /*
1884 * Note that 'og:url' may be provided instead of 'product:url'.
1885 *
1886 * Note that there is no Schema ean property.
1887 *
1888 * Note that there is no Schema size property.
1889 */
1890 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $mt_single, array(
1891 'url' => 'product:url',
1892 'name' => 'product:title',
1893 'description' => 'product:description',
1894 'category' => 'product:category', // Product category ID from Google product taxonomy.
1895 'sku' => 'product:retailer_part_no', // Product SKU.
1896 'mpn' => 'product:mfr_part_no', // Product MPN.
1897 'gtin14' => 'product:gtin14', // Valid for both products and offers.
1898 'gtin13' => 'product:gtin13', // Valid for both products and offers.
1899 'gtin12' => 'product:gtin12', // Valid for both products and offers.
1900 'gtin8' => 'product:gtin8', // Valid for both products and offers.
1901 'gtin' => 'product:gtin', // Valid for both products and offers.
1902 'availability' => 'product:availability', // Only valid for offers.
1903 'itemCondition' => 'product:condition', // Valid for both products and offers.
1904 'hasAdultConsideration' => 'product:adult_type', // Valid for both products and offers.
1905 'price' => 'product:price:amount',
1906 'priceCurrency' => 'product:price:currency',
1907 'priceValidUntil' => 'product:sale_price_dates:end',
1908 ) );
1909
1910 /*
1911 * Fallback to the 'og:url' value, if one is available.
1912 */
1913 if ( empty( $json_ret[ 'url' ] ) && ! empty( $mt_single[ 'og:url' ] ) ) {
1914
1915 $json_ret[ 'url' ] = $mt_single[ 'og:url' ];
1916 }
1917
1918 /*
1919 * Convert a numeric category ID to its Google category string.
1920 */
1921 WpssoSchema::check_prop_value_category( $json_ret );
1922
1923 WpssoSchema::check_prop_value_gtin( $json_ret );
1924
1925 /*
1926 * Prevents a missing property warning from the Google validator.
1927 */
1928 if ( empty( $json_ret[ 'priceValidUntil' ] ) ) {
1929
1930 $json_ret[ 'priceValidUntil' ] = WpssoSchema::get_schema_product_price_valid_date();
1931 }
1932
1933 /*
1934 * Schema priceSpecification property.
1935 *
1936 * Create the priceSpecification for the current pricing - this could be a list price or a sale price.
1937 */
1938 if ( apply_filters( 'wpsso_add_schema_unit_price_specification', true ) ) {
1939
1940 if ( false !== ( $price_spec = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
1941 'priceType' => 'product:price:type',
1942 'price' => 'product:price:amount',
1943 'priceCurrency' => 'product:price:currency',
1944 'validFrom' => 'product:sale_price_dates:start',
1945 'validThrough' => 'product:sale_price_dates:end',
1946 'valueAddedTaxIncluded' => 'product:price:vat_included',
1947 ) ) ) ) {
1948
1949 /*
1950 * Google only supports the Schem,a StrikethroughPrice type - remove other types like SalePrice or ListPrice.
1951 *
1952 * See https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example.
1953 */
1954 if ( isset( $price_spec[ 'priceType' ] ) && 'https://schema.org/StrikethroughPrice' !== $price_spec[ 'priceType' ] ) {
1955
1956 unset( $price_spec[ 'priceType' ] );
1957
1958 /*
1959 * If we're using price specifications, remove the main price information.
1960 */
1961 unset ( $json_ret[ 'price' ], $json_ret[ 'priceCurrency' ] );
1962 }
1963
1964 if ( empty( $price_spec[ 'priceCurrency' ] ) ) { // Make sure we have a price currency.
1965
1966 $price_spec[ 'priceCurrency' ] = $wpsso->options[ 'og_def_currency' ];
1967 }
1968
1969 if ( empty( $price_spec[ 'validThrough' ] ) ) { // Avoid Google validator warnings.
1970
1971 $price_spec[ 'validThrough' ] = WpssoSchema::get_schema_product_price_valid_date();
1972 }
1973
1974 /*
1975 * See http://wiki.goodrelations-vocabulary.org/Documentation/UN/CEFACT_Common_Codes.
1976 */
1977 $quantity = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
1978 'value' => 'product:eligible_quantity:value',
1979 'minValue' => 'product:eligible_quantity:min_value',
1980 'maxValue' => 'product:eligible_quantity:max_value',
1981 'unitCode' => 'product:eligible_quantity:unit_code',
1982 'unitText' => 'product:eligible_quantity:unit_text',
1983 ) );
1984
1985 if ( false !== $quantity ) {
1986
1987 if ( ! isset( $quantity[ 'value' ] ) ) {
1988
1989 if ( isset( $quantity[ 'minValue' ] ) && isset( $quantity[ 'maxValue' ] ) &&
1990 $quantity[ 'minValue' ] === $quantity[ 'maxValue' ] ) {
1991
1992 if ( $wpsso->debug->enabled ) {
1993
1994 $wpsso->debug->log( 'identical minValue and maxValue', $quantity );
1995 }
1996
1997 $quantity[ 'value' ] = $quantity[ 'minValue' ];
1998
1999 unset( $quantity[ 'minValue' ], $quantity[ 'maxValue' ] );
2000 }
2001 }
2002
2003 $price_spec[ 'eligibleQuantity' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/QuantitativeValue', $quantity );
2004 }
2005
2006 $json_ret[ 'priceSpecification' ][] = WpssoSchema::get_schema_type_context( 'https://schema.org/UnitPriceSpecification', $price_spec );
2007
2008 /*
2009 * If we have an original price (ie. regular or list price), and the original price type is
2010 * different to the current price type, then add the original price as well. Do not add the
2011 * eligible quantity, as the eligible quantity for the current sale (for example) may be different
2012 * than the original eligible quantity.
2013 */
2014 if ( ! empty( $mt_single[ 'product:original_price:type' ] ) && $mt_single[ 'product:original_price:type' ] != $mt_single[ 'product:price:type' ] ) {
2015
2016 if ( false !== ( $price_spec = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
2017 'priceType' => 'product:original_price:type',
2018 'price' => 'product:original_price:amount',
2019 'priceCurrency' => 'product:original_price:currency',
2020 'valueAddedTaxIncluded' => 'product:price:vat_included',
2021 ) ) ) ) {
2022
2023 /*
2024 * Google only supports the Schem,a StrikethroughPrice type - remove other types like SalePrice or ListPrice.
2025 *
2026 * See https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example.
2027 */
2028 if ( isset( $price_spec[ 'priceType' ] ) && 'https://schema.org/SalePrice' === $price_spec[ 'priceType' ] ) {
2029
2030 unset( $price_spec[ 'priceType' ] );
2031 }
2032
2033 if ( empty( $price_spec[ 'priceCurrency' ] ) ) { // Make sure we have a price currency.
2034
2035 $price_spec[ 'priceCurrency' ] = $wpsso->options[ 'og_def_currency' ];
2036 }
2037
2038 $json_ret[ 'priceSpecification' ][] = WpssoSchema::get_schema_type_context( 'https://schema.org/UnitPriceSpecification',
2039 $price_spec );
2040 }
2041 }
2042 }
2043
2044 } elseif ( $wpsso->debug->enabled ) {
2045
2046 $wpsso->debug->log( 'skipped schema unit price specification: filter returned false' );
2047 }
2048
2049 /*
2050 * Schema shippingDetails property.
2051 */
2052 if ( empty( $mt_single[ 'product:shipping_offers' ] ) ) {
2053
2054 if ( $wpsso->debug->enabled ) {
2055
2056 $wpsso->debug->log( 'product shipping offers is empty' );
2057 }
2058
2059 } elseif ( is_array( $mt_single[ 'product:shipping_offers' ] ) ) {
2060
2061 foreach ( $mt_single[ 'product:shipping_offers' ] as $ship_num => $ship_offer ) {
2062
2063 if ( ! is_array( $ship_offer ) ) { // Just in case.
2064
2065 if ( $wpsso->debug->enabled ) {
2066
2067 $wpsso->debug->log( 'skipping shipping #' . $ship_num . ': not an array' );
2068 }
2069
2070 continue;
2071 }
2072
2073 $ship_details = self::get_shipping_offer_data( $mod, $ship_offer, $json_ret[ 'url' ] );
2074
2075 if ( false === $ship_details ) {
2076
2077 continue;
2078 }
2079
2080 $json_ret[ 'shippingDetails' ][] = $ship_details;
2081 }
2082 }
2083
2084 /*
2085 * Schema hasMerchantReturnPolicy property.
2086 */
2087 $mrp_id = isset( $mt_single[ 'product:mrp_id' ] ) ? $mt_single[ 'product:mrp_id' ] : null;
2088
2089 if ( $wpsso->debug->enabled ) {
2090
2091 $wpsso->debug->log( 'applying filter \'wpsso_product_offer_mrp_id\'' );
2092 }
2093
2094 $mrp_id = apply_filters( 'wpsso_product_offer_mrp_id', $mrp_id, $mod );
2095
2096 if ( WpssoSchema::is_valid_val( $mrp_id ) ) { // Not null, an empty string, or 'none'.
2097
2098 if ( $wpsso->debug->enabled ) {
2099
2100 $wpsso->debug->log( 'product return policy id = ' . $mrp_id );
2101 }
2102
2103 self::add_merchant_return_policy_data( $json_ret[ 'hasMerchantReturnPolicy' ], $mod, $mt_single[ 'product:mrp_id' ], $mrp_list_el = false );
2104
2105 } elseif ( $wpsso->debug->enabled ) {
2106
2107 $wpsso->debug->log( 'no product return policy id' );
2108 }
2109
2110 /*
2111 * Add the seller organization data.
2112 */
2113 switch ( $wpsso->options[ 'site_pub_schema_type' ] ) {
2114
2115 case 'organization':
2116
2117 self::add_organization_data( $json_ret[ 'seller' ], $mod, $org_id = 'site', $org_logo_key = 'org_logo_url', $org_list_el = false );
2118
2119 break;
2120
2121 case 'person':
2122
2123 $user_id = $wpsso->options[ 'site_pub_person_id' ]; // 'none' by default.
2124
2125 if ( ! empty( $user_id ) && 'none' !== $user_id ) { // Just in case.
2126
2127 self::add_person_data( $json_ret[ 'seller' ], $mod, $user_id, $person_list_el = false );
2128 }
2129
2130 break;
2131 }
2132
2133 /*
2134 * Filter the single offer data.
2135 */
2136 if ( $wpsso->debug->enabled ) {
2137
2138 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_offer"' );
2139 }
2140
2141 $json_ret = apply_filters( 'wpsso_json_data_single_offer', $json_ret, $mod );
2142
2143 /*
2144 * Add or replace the json data.
2145 */
2146 self::add_or_replace_data( $json_data, $json_ret, $list_el );
2147
2148 return 1; // Return count of product offers added.
2149 }
2150
2151 /*
2152 * Adds the 'productGroupID', 'hasVariant', and 'variesBy' properties.
2153 *
2154 * See WpssoJsonTypeProductGroup->filter_json_data_https_schema_org_productgroup().
2155 */
2156 public static function add_product_group_data_mt( &$json_data, array $mod, array $mt_single, $def_type_id = 'product.group', $list_el = true ) {
2157
2158 $wpsso =& Wpsso::get_instance();
2159
2160 if ( $wpsso->debug->enabled ) {
2161
2162 $wpsso->debug->mark();
2163 }
2164
2165 /*
2166 * The 'product:retailer_item_id' value is required for the 'productGroupID' property.
2167 */
2168 if ( empty( $mt_single[ 'product:retailer_item_id' ] ) || ! is_numeric( $mt_single[ 'product:retailer_item_id' ] ) ) {
2169
2170 if ( $wpsso->debug->enabled ) {
2171
2172 $wpsso->debug->log( 'exiting early: invalid retailer item id' );
2173 }
2174
2175 return 0;
2176 }
2177
2178 /*
2179 * If not adding a list element, get the existing schema type url (if one exists).
2180 */
2181 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id, $list_el );
2182
2183 /*
2184 * Begin schema product markup creation.
2185 */
2186 $json_ret = WpssoSchema::get_schema_type_context( $type_url, array(
2187 'productGroupID' => $mt_single[ 'product:retailer_item_id' ],
2188 ) );
2189
2190 /*
2191 * Set reference values for admin notices.
2192 */
2193 if ( is_admin() ) {
2194
2195 $canonical_url = $wpsso->util->get_canonical_url( $mod );
2196
2197 $wpsso->util->maybe_set_ref( $canonical_url, $mod, __( 'adding schema product group', 'wpsso' ) );
2198 }
2199
2200 /*
2201 * Add the product group variants.
2202 */
2203 if ( ! empty( $mt_single[ 'product:variants' ] ) && is_array( $mt_single[ 'product:variants' ] ) ) {
2204
2205 WpssoSchema::add_variants_data_mt( $json_ret, $mt_single[ 'product:variants' ] );
2206 }
2207
2208 /*
2209 * Add the variesBy property.
2210 *
2211 * Indicates the property or properties by which the variants in a ProductGroup vary, e.g. their size,
2212 * color etc. Schema.org properties can be referenced by their short name e.g. "color"; terms defined
2213 * elsewhere can be referenced with their URIs.
2214 */
2215 if ( ! empty( $json_ret[ 'hasVariant' ] ) ) { // Just in case.
2216
2217 $varies_by = array();
2218 $incl_varies_by = array();
2219 $excl_varies_by = array_keys( $wpsso->cf[ 'form' ][ 'excl_varies_by_props' ] );
2220
2221 /*
2222 * Get the property names used by all variants.
2223 */
2224 foreach ( $json_ret[ 'hasVariant' ] as $variant ) {
2225
2226 $incl_varies_by = array_merge( $incl_varies_by, array_keys( $variant ) );
2227 }
2228
2229 $incl_varies_by = array_unique( $incl_varies_by );
2230
2231 /*
2232 * Get the property names that are not excluded.
2233 */
2234 if ( ! empty( $excl_varies_by ) && is_array( $excl_varies_by ) ) { // Just in case.
2235
2236 $incl_varies_by = array_diff( $incl_varies_by, $excl_varies_by );
2237 }
2238
2239 foreach ( $incl_varies_by as $prop_name ) {
2240
2241 $last_value = null;
2242
2243 foreach ( $json_ret[ 'hasVariant' ] as $num => $variant ) {
2244
2245 if ( ! isset( $variant[ $prop_name ] ) ) { // If not set in one variant, then it automatically varies.
2246
2247 $varies_by[] = $prop_name;
2248
2249 continue 2;
2250
2251 } elseif ( null !== $last_value ) { // We must have at least one previous value to compare.
2252
2253 if ( $last_value !== $variant[ $prop_name ] ) {
2254
2255 $varies_by[] = $prop_name;
2256
2257 continue 2;
2258 }
2259 }
2260
2261 $last_value = $variant[ $prop_name ];
2262 }
2263 }
2264
2265 if ( $wpsso->debug->enabled ) {
2266
2267 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_product_group_varies_by"' );
2268 }
2269
2270 $varies_by = apply_filters( 'wpsso_json_data_single_product_group_varies_by', $varies_by, $mod );
2271
2272 if ( ! empty( $varies_by ) ) {
2273
2274 $json_ret[ 'variesBy' ] = $varies_by;
2275 }
2276 }
2277
2278 /*
2279 * Filter the single product group data.
2280 */
2281 if ( $wpsso->debug->enabled ) {
2282
2283 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_product_group"' );
2284 }
2285
2286 $json_ret = apply_filters( 'wpsso_json_data_single_product_group', $json_ret, $mod );
2287
2288 /*
2289 * Restore previous reference values for admin notices.
2290 */
2291 if ( is_admin() ) {
2292
2293 $wpsso->util->maybe_unset_ref( $canonical_url );
2294 }
2295
2296 /*
2297 * Add or replace the json data.
2298 */
2299 self::add_or_replace_data( $json_data, $json_ret, $list_el );
2300
2301 return 1; // Return count of product groups added.
2302 }
2303
2304 /*
2305 * See WpssoSchemaSingle::get_product_data_mt().
2306 * See WpssoJsonTypeProduct->filter_json_data_https_schema_org_product().
2307 */
2308 public static function add_product_data_mt( &$json_data, array $mod, array $mt_single, $def_type_id = 'product', $list_el = true ) {
2309
2310 $wpsso =& Wpsso::get_instance();
2311
2312 if ( $wpsso->debug->enabled ) {
2313
2314 $wpsso->debug->mark();
2315 }
2316
2317 /*
2318 * If not adding a list element, get the existing schema type url (if one exists).
2319 */
2320 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id, $list_el );
2321
2322 /*
2323 * Begin schema product markup creation.
2324 */
2325 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
2326
2327 /*
2328 * Set reference values for admin notices.
2329 */
2330 if ( is_admin() ) {
2331
2332 $canonical_url = $wpsso->util->get_canonical_url( $mod );
2333
2334 $wpsso->util->maybe_set_ref( $canonical_url, $mod, __( 'adding schema product', 'wpsso' ) );
2335 }
2336
2337 /*
2338 * Note that there is no Schema availability property for the 'product:availability' value.
2339 *
2340 * Note that there is no Schema ean property for the 'product:ean' value.
2341 *
2342 * See https://support.google.com/merchants/answer/6324507 for 'inProductGroupWithID'.
2343 */
2344 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $mt_single, array(
2345 'url' => 'product:url',
2346 'name' => 'product:title',
2347 'description' => 'product:description',
2348 'category' => 'product:category', // Product category ID from Google product taxonomy.
2349 'sku' => 'product:retailer_part_no', // Product SKU.
2350 'mpn' => 'product:mfr_part_no', // Product MPN.
2351 'inProductGroupWithID' => 'product:item_group_id',
2352 'gtin14' => 'product:gtin14', // Valid for both products and offers.
2353 'gtin13' => 'product:gtin13', // Valid for both products and offers.
2354 'gtin12' => 'product:gtin12', // Valid for both products and offers.
2355 'gtin8' => 'product:gtin8', // Valid for both products and offers.
2356 'gtin' => 'product:gtin', // Valid for both products and offers.
2357 'itemCondition' => 'product:condition', // Valid for both products and offers.
2358 'hasAdultConsideration' => 'product:adult_type', // Valid for both products and offers.
2359 'color' => 'product:color',
2360 'material' => 'product:material',
2361 'pattern' => 'product:pattern',
2362 'award' => 'product:awards',
2363 ) );
2364
2365 /*
2366 * Convert a numeric category ID to its Google category string.
2367 */
2368 WpssoSchema::check_prop_value_category( $json_ret );
2369
2370 WpssoSchema::check_prop_value_gtin( $json_ret );
2371
2372 /*
2373 * Schema productID property.
2374 */
2375 foreach ( array( 'isbn' ) as $pref_id ) {
2376
2377 if ( WpssoSchema::is_valid_key( $mt_single, 'product:' . $pref_id ) ) { // Not null, an empty string, or 'none'.
2378
2379 $json_ret[ 'productID' ] = $pref_id . ':' . $mt_single[ 'product:' . $pref_id ];
2380
2381 break; // Stop here.
2382 }
2383 }
2384
2385 /*
2386 * Schema brand property.
2387 *
2388 * Note that product group variants will automatically inherit the brand property from the main product.
2389 *
2390 * See WpssoConfig::$cf[ 'form' ][ 'inherit_variant_props' ].
2391 * See WpssoJsonTypeProductGroup->filter_json_data_https_schema_org_productgroup().
2392 */
2393 if ( WpssoSchema::is_valid_key( $mt_single, 'product:brand' ) ) { // Not null, an empty string, or 'none'.
2394
2395 $brand = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
2396 'name' => 'product:brand',
2397 ) );
2398
2399 if ( false !== $brand ) { // Just in case.
2400
2401 $json_ret[ 'brand' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/Brand', $brand );
2402 }
2403
2404 } elseif ( $wpsso->debug->enabled ) {
2405
2406 $wpsso->debug->log( 'no product brand' );
2407 }
2408
2409 /*
2410 * Schema audience property.
2411 *
2412 * See https://support.google.com/merchants/answer/6324479 for 'suggestedGender'.
2413 * See https://support.google.com/merchants/answer/6324463 for 'suggestedMinAge' and 'suggestedMaxAge'.
2414 */
2415 $audience = array();
2416
2417 if ( WpssoSchema::is_valid_key( $mt_single, 'product:target_gender' ) ) { // Not null, an empty string, or 'none'.
2418
2419 $audience[ 'suggestedGender' ] = $mt_single[ 'product:target_gender' ];
2420
2421 } elseif ( $wpsso->debug->enabled ) {
2422
2423 $wpsso->debug->log( 'no product gender' );
2424 }
2425
2426 if ( WpssoSchema::is_valid_key( $mt_single, 'product:age_group' ) ) { // Not null, an empty string, or 'none'.
2427
2428 /*
2429 * Age is expressed in years so, for example, use 0.25 for 3 months.
2430 *
2431 * See https://support.google.com/merchants/answer/6324463.
2432 */
2433 switch ( $mt_single[ 'product:age_group' ] ) {
2434 case 'adult': $audience[ 'suggestedMinAge' ] = 13; break;
2435 case 'all ages': $audience[ 'suggestedMinAge' ] = 13; break;
2436 case 'infant': $audience[ 'suggestedMinAge' ] = 0.25; $audience[ 'suggestedMaxAge' ] = 1; break;
2437 case 'kids': $audience[ 'suggestedMinAge' ] = 5; $audience[ 'suggestedMaxAge' ] = 13; break;
2438 case 'newborn': $audience[ 'suggestedMinAge' ] = 0; $audience[ 'suggestedMaxAge' ] = 0.25; break;
2439 case 'teen': $audience[ 'suggestedMinAge' ] = 13; break;
2440 case 'toddler': $audience[ 'suggestedMinAge' ] = 1; $audience[ 'suggestedMaxAge' ] = 5; break;
2441 }
2442
2443 } elseif ( $wpsso->debug->enabled ) {
2444
2445 $wpsso->debug->log( 'no product age group' );
2446 }
2447
2448 if ( ! empty( $audience ) ) {
2449
2450 $json_ret[ 'audience' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/PeopleAudience', $audience );
2451 }
2452
2453 /*
2454 * Schema size property.
2455 *
2456 * See https://support.google.com/merchants/answer/6324492 for 'name'.
2457 * See https://support.google.com/merchants/answer/6324497 for 'sizeGroup'.
2458 * See https://support.google.com/merchants/answer/6324502 for 'sizeSystem'.
2459 */
2460 $size_spec = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
2461 'name' => 'product:size',
2462 'sizeGroup' => 'product:size_group',
2463 'sizeSystem' => 'product:size_system',
2464 ) );
2465
2466 if ( false !== $size_spec ) {
2467
2468 $json_ret[ 'size' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/SizeSpecification', $size_spec );
2469 }
2470
2471 /*
2472 * Schema length, width, height, weight properties.
2473 */
2474 WpssoSchema::add_data_unit_from_assoc( $json_ret, $mt_single, array(
2475 'length' => 'product:length:value',
2476 'width' => 'product:width:value',
2477 'height' => 'product:height:value',
2478 'weight' => 'product:weight:value',
2479 'fluid_volume' => 'product:fluid_volume:value',
2480 ) );
2481
2482 /*
2483 * Schema hasEnergyConsumptionDetails property.
2484 */
2485 if ( WpssoSchema::is_valid_key( $mt_single, 'product:energy_efficiency:value' ) ) { // Not null, an empty string, or 'none'.
2486
2487 $energy_efficiency = WpssoSchema::get_data_itemprop_from_assoc( $mt_single, array(
2488 'hasEnergyEfficiencyCategory' => 'product:energy_efficiency:value',
2489 'energyEfficiencyScaleMin' => 'product:energy_efficiency:min_value',
2490 'energyEfficiencyScaleMax' => 'product:energy_efficiency:max_value',
2491 ) );
2492
2493 if ( false !== $energy_efficiency ) { // Just in case.
2494
2495 $json_ret[ 'hasEnergyConsumptionDetails' ] = WpssoSchema::get_schema_type_context( 'https://schema.org/EnergyConsumptionDetails',
2496 $energy_efficiency );
2497 }
2498
2499 } elseif ( $wpsso->debug->enabled ) {
2500
2501 $wpsso->debug->log( 'no product energy efficiency' );
2502 }
2503
2504 /*
2505 * See https://schema.org/image as https://schema.org/ImageObject.
2506 * See https://schema.org/subjectOf as https://schema.org/VideoObject.
2507 */
2508 if ( $wpsso->debug->enabled ) {
2509
2510 $wpsso->debug->log( 'adding image and subjectOf video properties for product' );
2511 }
2512
2513 WpssoSchema::add_media_data( $json_ret, $mod, $mt_single, $size_names = 'schema', $add_video = 'subjectOf' );
2514
2515 /*
2516 * Prevent recursion for an itemOffered within a Schema Offer.
2517 */
2518 static $local_recursion = false;
2519
2520 if ( ! $local_recursion ) {
2521
2522 $local_recursion = true;
2523
2524 if ( empty( $mt_single[ 'product:offers' ] ) ) {
2525
2526 $json_ret[ 'offers' ] = self::get_offer_data_mt( $mod, $mt_single, $def_type_id = 'offer' );
2527
2528 } elseif ( is_array( $mt_single[ 'product:offers' ] ) ) {
2529
2530 if ( empty( $wpsso->options[ 'schema_def_product_aggr_offers' ] ) ) {
2531
2532 WpssoSchema::add_offers_data_mt( $json_ret, $mt_single[ 'product:offers' ] );
2533
2534 } else WpssoSchema::add_offers_aggregate_data_mt( $json_ret, $mt_single[ 'product:offers' ] );
2535 }
2536
2537 $local_recursion = false;
2538 }
2539
2540 /*
2541 * Check for required Product properties.
2542 *
2543 * The "image" property is required for Google's Merchant listings validator.
2544 */
2545 WpssoSchema::check_required_props( $json_ret, $mod, array( 'image' ), $type_id );
2546
2547 /*
2548 * Filter the single product data.
2549 */
2550 if ( $wpsso->debug->enabled ) {
2551
2552 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_product"' );
2553 }
2554
2555 $json_ret = apply_filters( 'wpsso_json_data_single_product', $json_ret, $mod );
2556
2557 /*
2558 * Restore previous reference values for admin notices.
2559 */
2560 if ( is_admin() ) {
2561
2562 $wpsso->util->maybe_unset_ref( $canonical_url );
2563 }
2564
2565 /*
2566 * Update the @id string with the $json_ret[ 'url' ] and $type_id.
2567 */
2568 WpssoSchema::update_data_id( $json_ret, empty( $mod[ 'id' ] ) ? $type_id : array( $type_id, $mod[ 'id' ] ) );
2569
2570 /*
2571 * Add or replace the json data.
2572 */
2573 self::add_or_replace_data( $json_data, $json_ret, $list_el );
2574
2575 return 1; // Return count of products added.
2576 }
2577
2578 public static function add_service_data( &$json_data, array $mod, $service_id, $list_el = false ) {
2579
2580 $wpsso =& Wpsso::get_instance();
2581
2582 if ( $wpsso->debug->enabled ) {
2583
2584 $wpsso->debug->mark();
2585 }
2586
2587 /*
2588 * Maybe get options from integration modules.
2589 *
2590 * Returned options can change depending on the locale, but the option key names should NOT be localized.
2591 */
2592 $service_opts = apply_filters( 'wpsso_get_service_options', false, $mod, $service_id );
2593
2594 if ( ! empty( $service_opts ) ) {
2595
2596 if ( $wpsso->debug->enabled ) {
2597
2598 $wpsso->debug->log_arr( 'get_service_options', $service_opts );
2599 }
2600 }
2601
2602 /*
2603 * Add metadata defaults and custom values to the $service_opts array.
2604 *
2605 * Automatically renames 'schema_service_*' options from the Document SSO metabox to 'service_*'.
2606 */
2607 WpssoSchema::add_type_opts_md_pad( $service_opts, $mod, array( 'service' => 'schema_service' ) );
2608
2609 if ( empty( $service_opts ) ) {
2610
2611 if ( $wpsso->debug->enabled ) {
2612
2613 $wpsso->debug->log( 'exiting early: no service options' );
2614 }
2615
2616 return 0; // Return count of services added.
2617 }
2618
2619 /*
2620 * If not adding a list element, get the existing schema type url (if one exists).
2621 */
2622 list( $type_id, $type_url ) = self::get_data_types( $json_data, $service_opts, $opt_key = 'service_schema_type', $def_type_id = 'service', $list_el );
2623
2624 /*
2625 * Begin schema service markup creation.
2626 */
2627 $json_ret = WpssoSchema::get_schema_type_context( $type_url );
2628
2629 /*
2630 * Add area served property.
2631 */
2632 WpssoSchema::add_type_data_areaserved( $json_ret, $mod, $service_opts, $service_id, 'service' );
2633
2634 /*
2635 * Add place, organization, and person data.
2636 *
2637 * Use $opt_pre => $prop_name association as the property name may be repeated (ie. non-unique).
2638 */
2639 foreach ( array(
2640 'service_prov_org_id' => 'provider', // Provider Organization.
2641 'service_prov_person_id' => 'provider', // Provider Person.
2642 ) as $opt_pre => $prop_name ) {
2643
2644 foreach ( SucomUtil::get_multi_values( $service_opts, $opt_pre ) as $id ) { // Uses is_valid_option_value().
2645
2646 if ( strpos( $opt_pre, '_org_id' ) ) {
2647
2648 $org_logo_key = 'org_logo_url';
2649
2650 self::add_organization_data( $json_ret[ $prop_name ], $mod, $id, $org_logo_key, $org_list_el = true );
2651
2652 } elseif ( strpos( $opt_pre, '_person_id' ) ) {
2653
2654 self::add_person_data( $json_ret[ $prop_name ], $mod, $id, $person_list_el = true );
2655 }
2656 }
2657 }
2658
2659 /*
2660 * Add schema properties from the organization options.
2661 */
2662 SucomUtil::add_multi_values( $service_opts, 'service_award', 'service_awards' );
2663
2664 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $service_opts, array(
2665 'award' => 'service_awards', // Service Awards.
2666 ) );
2667
2668 /*
2669 * Add service offers.
2670 */
2671 WpssoSchema::add_type_data_offers( $json_ret, $mod, $service_opts, $service_id, 'service' );
2672
2673 /*
2674 * See https://schema.org/hasOfferCatalog.
2675 */
2676 WpssoSchema::add_offer_catalogs_data( $json_ret, $mod, $service_opts, 'service_offer_catalog', 'hasOfferCatalog' );
2677
2678 /*
2679 * Filter the single service data.
2680 */
2681 if ( $wpsso->debug->enabled ) {
2682
2683 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_service"' );
2684 }
2685
2686 $json_ret = apply_filters( 'wpsso_json_data_single_service', $json_ret, $mod, $service_id );
2687
2688 /*
2689 * Update the @id string with the $json_ret[ 'url' ], $type_id, and $service_id values.
2690 */
2691 WpssoSchema::update_data_id( $json_ret, array( $type_id, $service_id ) );
2692
2693 /*
2694 * Add or replace the json data.
2695 */
2696 self::add_or_replace_data( $json_data, $json_ret, $list_el );
2697
2698 return 1; // Return count of services added.
2699 }
2700
2701 /*
2702 * Pass a single dimension video array in $mt_single.
2703 *
2704 * Example $mt_single array:
2705 *
2706 * Array (
2707 * [og:video:title] => An Example Title
2708 * [og:video:description] => An example description...
2709 * [og:video:secure_url] => https://vimeo.com/moogaloop.swf?clip_id=150575335&autoplay=1
2710 * [og:video:url] => http://vimeo.com/moogaloop.swf?clip_id=150575335&autoplay=1
2711 * [og:video:type] => application/x-shockwave-flash
2712 * [og:video:width] => 1280
2713 * [og:video:height] => 544
2714 * [og:video:embed_url] => https://player.vimeo.com/video/150575335?autoplay=1
2715 * [og:video:has_image] => 1
2716 * [og:image:secure_url] => https://i.vimeocdn.com/video/550095036_1280.jpg
2717 * [og:image:url] => http://i.vimeocdn.com/video/550095036_1280.jpg
2718 * [og:image:url] =>
2719 * [og:image:width] => 1280
2720 * [og:image:height] => 544
2721 * )
2722 */
2723 public static function add_video_data_mt( &$json_data, array $mt_single, $mt_pre = 'og:video', $list_el = true ) {
2724
2725 $wpsso =& Wpsso::get_instance();
2726
2727 if ( $wpsso->debug->enabled ) {
2728
2729 $wpsso->debug->mark();
2730 }
2731
2732 if ( empty( $mt_single ) || ! is_array( $mt_single ) ) {
2733
2734 if ( $wpsso->debug->enabled ) {
2735
2736 $wpsso->debug->log( 'exiting early: options array is empty or not an array' );
2737 }
2738
2739 return 0; // Return count of videos added.
2740 }
2741
2742 $media_url = SucomUtil::get_first_mt_media_url( $mt_single, $mt_pre );
2743
2744 if ( empty( $media_url ) ) {
2745
2746 if ( $wpsso->debug->enabled ) {
2747
2748 $wpsso->debug->log( 'exiting early: ' . $mt_pre . ' URL values are empty' );
2749 }
2750
2751 return 0; // Return count of videos added.
2752 }
2753
2754 /*
2755 * If not adding a list element, get the existing schema type url (if one exists).
2756 */
2757 list( $type_id, $type_url ) = self::get_data_types( $json_data, $type_opts = false, $opt_key = false, $def_type_id = 'video.object', $list_el );
2758
2759 /*
2760 * Begin schema video markup creation.
2761 */
2762 $json_ret = WpssoSchema::get_schema_type_context( $type_url, array(
2763 'url' => SucomUtil::esc_url_encode( $media_url ),
2764 ) );
2765
2766 WpssoSchema::add_data_itemprop_from_assoc( $json_ret, $mt_single, array(
2767 'name' => $mt_pre . ':title',
2768 'description' => $mt_pre . ':description',
2769 'embedUrl' => $mt_pre . ':embed_url',
2770 'contentUrl' => $mt_pre . ':stream_url',
2771 'contentSize' => $mt_pre . ':stream_size',
2772 'encodingFormat' => $mt_pre . ':type', // Mime type.
2773 'duration' => $mt_pre . ':duration',
2774 'uploadDate' => $mt_pre . ':upload_date',
2775 'thumbnailUrl' => $mt_pre . ':thumbnail_url',
2776 ) );
2777
2778 /*
2779 * Add width and height as QuantitativeValue.
2780 */
2781 WpssoSchema::add_data_unit_from_assoc( $json_ret, $mt_single, array(
2782 'width_px' => $mt_pre . ':width',
2783 'height_px' => $mt_pre . ':height',
2784 ) );
2785
2786 if ( ! empty( $mt_single[ $mt_pre . ':has_image' ] ) ) {
2787
2788 self::add_image_data_mt( $json_ret[ 'thumbnail' ], $mt_single, null, $image_list_el = false );
2789 }
2790
2791 if ( ! empty( $mt_single[ $mt_pre . ':tag' ] ) ) {
2792
2793 if ( is_array( $mt_single[ $mt_pre . ':tag' ] ) ) {
2794
2795 $json_ret[ 'keywords' ] = implode( $glue = ', ', $mt_single[ $mt_pre . ':tag' ] );
2796
2797 } else {
2798
2799 $json_ret[ 'keywords' ] = $mt_single[ $mt_pre . ':tag' ];
2800 }
2801 }
2802
2803 /*
2804 * Update the @id string with the $json_ret[ 'url' ] and $type_id.
2805 */
2806 WpssoSchema::update_data_id( $json_ret, $type_id );
2807
2808 /*
2809 * Add or replace the json data.
2810 */
2811 self::add_or_replace_data( $json_data, $json_ret, $list_el );
2812
2813 return 1; // Return count of videos added.
2814 }
2815
2816 /*
2817 * See WpssoSchema::add_offers_aggregate_data_mt().
2818 * See WpssoSchema::add_offers_data_mt().
2819 * See WpssoSchemaSingle::add_product_data_mt().
2820 * See WpssoJsonTypeSoftwareApplication->filter_json_data_https_schema_org_softwareapplication().
2821 */
2822 public static function get_offer_data_mt( array $mod, array $mt_single, $def_type_id = 'offer' ) {
2823
2824 $wpsso =& Wpsso::get_instance();
2825
2826 if ( $wpsso->debug->enabled ) {
2827
2828 $wpsso->debug->log_arr( 'mt_single', $mt_single );
2829 }
2830
2831 $json_data = array();
2832
2833 self::add_offer_data_mt( $json_data, $mod, $mt_single, $def_type_id = 'offer', $list_el = false );
2834
2835 return $json_data;
2836 }
2837
2838 /*
2839 * See WpssoSchema::add_variants_data_mt().
2840 */
2841 public static function get_product_data_mt( array $mod, $mt_single, $def_type_id = 'product' ) {
2842
2843 $wpsso =& Wpsso::get_instance();
2844
2845 if ( $wpsso->debug->enabled ) {
2846
2847 $wpsso->debug->log_arr( 'mt_single', $mt_single );
2848 }
2849
2850 $json_data = array();
2851
2852 self::add_product_data_mt( $json_data, $mod, $mt_single, $def_type_id = 'product', $list_el = false );
2853
2854 return $json_data;
2855 }
2856
2857 /*
2858 * Returns OfferShippingDetails with shippingDestination and shippingRate properties.
2859 *
2860 * See https://developers.google.com/search/docs/data-types/product#shipping-details-best-practices.
2861 */
2862 public static function get_shipping_offer_data( array $mod, array $ship_offer, $offer_url = '' ) {
2863
2864 $wpsso =& Wpsso::get_instance();
2865
2866 if ( $wpsso->debug->enabled ) {
2867
2868 $wpsso->debug->mark();
2869 }
2870
2871 $json_data = WpssoSchema::get_schema_type_context( 'https://schema.org/OfferShippingDetails' );
2872
2873 /*
2874 * An @id property is added at the end from the 'shipping_id' and $offer_url values.
2875 */
2876 WpssoSchema::add_data_itemprop_from_assoc( $json_data, $ship_offer, array(
2877 'name' => 'shipping_name',
2878 ) );
2879
2880 if ( isset( $ship_offer[ 'shipping_destinations' ] ) ) {
2881
2882 /*
2883 * Each destination options array can include an array of countries, a single country, or a single
2884 * country and state - all with postal code limits, if any were found above.
2885 *
2886 * See https://developers.google.com/search/docs/data-types/product#shipping-details-best-practices.
2887 */
2888 $dest_keys = array(
2889 'country_code' => 'addressCountry', // Can be a string or an array.
2890 'region_code' => 'addressRegion',
2891 'postal_code' => 'postalCode', // Can be a string or an array.
2892 );
2893
2894 foreach ( $ship_offer[ 'shipping_destinations' ] as $dest_num => $dest_opts ) {
2895
2896 $defined_region = array();
2897
2898 /*
2899 * For each option key, assign its value to the associated Schema property name.
2900 *
2901 * If the option key is a postal code array, then check each value for a wildcard or range.
2902 * If a wildcard or range is found, then assign its value to the postalCodeRange property
2903 * instead.
2904 */
2905 foreach ( $dest_opts as $opt_key => $val ) {
2906
2907 if ( ! isset( $dest_keys[ $opt_key ] ) ) { // Make sure we have a Schema property name.
2908
2909 continue;
2910 }
2911
2912 $prop_name = $dest_keys[ $opt_key ];
2913
2914 /*
2915 * Check for wildcards and ranges in postal codes.
2916 */
2917 if ( 'postal_code' === $opt_key ) {
2918
2919 if ( ! is_array( $val ) ) { // Just in case.
2920
2921 $val = array( $val );
2922 }
2923
2924 foreach ( $val as $num => $postal_code ) {
2925
2926 /*
2927 * Note that wildcards and ranges cannot be mixed, and ranges only
2928 * work with postal codes that are numeric (ie. US zip codes).
2929 *
2930 * Example:
2931 *
2932 * H*
2933 * 96200...96600
2934 */
2935 if ( preg_match( '/^(.+)\*+$/', $postal_code, $matches ) ||
2936 preg_match( '/^(.+)\.\.\.(.+)$/', $postal_code, $matches ) ) {
2937
2938 $postal_code_range = WpssoSchema::get_schema_type_context(
2939 'https://schema.org/PostalCodeRangeSpecification',
2940 array( 'postalCodeBegin' => $matches[ 1 ] )
2941 );
2942
2943 if ( isset( $matches[ 2 ] ) ) {
2944
2945 $postal_code_range[ 'postalCodeEnd' ] = $matches[ 2 ];
2946 }
2947
2948 $defined_region[ 'postalCodeRange' ][] = $postal_code_range;
2949
2950 } elseif ( ! empty( $postal_code ) ) {
2951
2952 $defined_region[ 'postalCode' ][] = $postal_code;
2953 }
2954 }
2955
2956 } else $defined_region[ $prop_name ] = $val;
2957 }
2958
2959 if ( ! empty( $defined_region ) ) { // Ships to the World.
2960
2961 if ( ! empty( $dest_opts[ 'destination_id' ] ) ) {
2962
2963 if ( ! empty( $dest_opts[ 'destination_rel' ] ) ) {
2964
2965 WpssoSchema::update_data_id( $defined_region, $dest_opts[ 'destination_id' ], $dest_opts[ 'destination_rel' ] );
2966
2967 } else WpssoSchema::update_data_id( $defined_region, $dest_opts[ 'destination_id' ], $offer_url );
2968 }
2969
2970 $json_data[ 'shippingDestination' ][] = WpssoSchema::get_schema_type_context(
2971 'https://schema.org/DefinedRegion', $defined_region );
2972 }
2973 }
2974 }
2975
2976 if ( isset( $ship_offer[ 'shipping_rate' ] ) ) {
2977
2978 /*
2979 * See https://developers.google.com/search/docs/data-types/product#shipping-details-best-practices.
2980 */
2981 $shipping_rate_keys = array(
2982 'shipping_rate_name' => 'name',
2983 'shipping_rate_cost' => 'value',
2984 'shipping_rate_currency' => 'currency',
2985 );
2986
2987 $shipping_rate = array();
2988
2989 foreach ( $ship_offer[ 'shipping_rate' ] as $opt_key => $val ) {
2990
2991 if ( isset( $shipping_rate_keys[ $opt_key ] ) ) {
2992
2993 $shipping_rate[ $shipping_rate_keys[ $opt_key ] ] = $val;
2994 }
2995 }
2996
2997 if ( ! empty( $shipping_rate ) ) {
2998
2999 $json_data[ 'shippingRate' ] = WpssoSchema::get_schema_type_context(
3000 'https://schema.org/MonetaryAmount', $shipping_rate );
3001 }
3002 }
3003
3004 /*
3005 * Example $ship_offer[ 'delivery_time' ] = Array (
3006 * [shipdept_rel] => http://adm.surniaulula.com/produit/a-variable-product/
3007 * [shipdept_timezone] => America/Vancouver
3008 * [shipdept_midday_close] => 12:00
3009 * [shipdept_midday_open] => 13:00
3010 * [shipdept_cutoff] => 16:00
3011 * [shipdept_day_sunday_open] => none
3012 * [shipdept_day_sunday_close] => none
3013 * [shipdept_day_monday_open] => 09:00
3014 * [shipdept_day_monday_close] => 17:00
3015 * [shipdept_day_tuesday_open] => 09:00
3016 * [shipdept_day_tuesday_close] => 17:00
3017 * [shipdept_day_wednesday_open] => 09:00
3018 * [shipdept_day_wednesday_close] => 17:00
3019 * [shipdept_day_thursday_open] => 09:00
3020 * [shipdept_day_thursday_close] => 17:00
3021 * [shipdept_day_friday_open] => 09:00
3022 * [shipdept_day_friday_close] => 17:00
3023 * [shipdept_day_saturday_open] => none
3024 * [shipdept_day_saturday_close] => none
3025 * [shipdept_day_publicholidays_open] => 09:00
3026 * [shipdept_day_publicholidays_close] => 12:00
3027 * [handling_rel] => http://adm.surniaulula.com/produit/a-variable-product/
3028 * [handling_maximum] => 1.5
3029 * [handling_unit_code] => DAY
3030 * [handling_unit_text] => d
3031 * [handling_name] => Days
3032 * [transit_rel] => http://adm.surniaulula.com/produit/a-variable-product/
3033 * [transit_minimum] => 5
3034 * [transit_maximum] => 7
3035 * [transit_unit_code] => DAY
3036 * [transit_unit_text] => d
3037 * [transit_name] => Days
3038 * )
3039 */
3040 if ( isset( $ship_offer[ 'delivery_time' ] ) ) {
3041
3042 $delivery_opts =& $ship_offer[ 'delivery_time' ];
3043
3044 if ( $wpsso->debug->enabled ) {
3045
3046 $wpsso->debug->log_arr( 'delivery_opts', $delivery_opts );
3047 }
3048
3049 /*
3050 * See https://schema.org/ShippingDeliveryTime.
3051 */
3052 $delivery_time = array();
3053
3054 /*
3055 * Property:
3056 * businessDays as https://schema.org/OpeningHoursSpecification
3057 */
3058 if ( $opening_hours_spec = self::get_opening_hours_data( $delivery_opts, $opt_prefix = 'shipdept' ) ) {
3059
3060 $delivery_time[ 'businessDays' ] = $opening_hours_spec;
3061 }
3062
3063 $cutoff_tz = SucomUtilOptions::get_opts_hm_tz( $delivery_opts, $key_hm = 'shipdept_cutoff', $key_tz = 'shipdept_timezone' );
3064
3065 if ( ! empty( $cutoff_tz ) ) {
3066
3067 $delivery_time[ 'cutoffTime' ] = $cutoff_tz;
3068 }
3069
3070 foreach ( array(
3071 'handling' => 'handlingTime',
3072 'transit' => 'transitTime'
3073 ) as $delivery_opt_pre => $delivery_prop_name ) {
3074
3075 if ( $wpsso->debug->enabled ) {
3076
3077 $wpsso->debug->log( 'getting ' . $delivery_opt_pre . ' options' );
3078 }
3079
3080 $quant_id = 'qv';
3081 $quantity = array();
3082
3083 /*
3084 * See http://wiki.goodrelations-vocabulary.org/Documentation/UN/CEFACT_Common_Codes.
3085 */
3086 foreach( array(
3087 $delivery_opt_pre . '_name' => 'name',
3088 $delivery_opt_pre . '_minimum' => 'minValue',
3089 $delivery_opt_pre . '_maximum' => 'maxValue',
3090 $delivery_opt_pre . '_unit_code' => 'unitCode',
3091 $delivery_opt_pre . '_unit_text' => 'unitText',
3092 ) as $opt_key => $quant_prop_name ) {
3093
3094 if ( isset( $delivery_opts[ $opt_key ] ) ) {
3095
3096 /*
3097 * Skip the name and unit text for the quantity @id value.
3098 */
3099 if ( 'name' !== $quant_prop_name && 'unitText' !== $quant_prop_name ) {
3100
3101 $quant_id .= '-' . $delivery_opts[ $opt_key ];
3102 }
3103
3104 $quantity[ $quant_prop_name ] = $delivery_opts[ $opt_key ];
3105
3106 } else $quant_id .= '--';
3107 }
3108
3109 /*
3110 * Google requires both values, but let the Google validator report if one value is missing.
3111 */
3112 if ( isset( $quantity[ 'minValue' ] ) || isset( $quantity[ 'maxValue' ] ) ) {
3113
3114 if ( ! empty( $quantity[ 'unitCode' ] ) ) {
3115
3116 $quant_id = strtolower( $quant_id );
3117
3118 $quant_rel = empty( $delivery_opts[ $delivery_opt_pre . '_rel' ] ) ?
3119 $offer_url : $delivery_opts[ $delivery_opt_pre . '_rel' ];
3120
3121 if ( $wpsso->debug->enabled ) {
3122
3123 $wpsso->debug->log_arr( 'adding data id ' . $quant_id, $quantity );
3124 }
3125
3126 WpssoSchema::update_data_id( $quantity, $quant_id, $quant_rel );
3127 }
3128
3129 $delivery_time[ $delivery_prop_name ] = WpssoSchema::get_schema_type_context(
3130 'https://schema.org/QuantitativeValue', $quantity );
3131
3132 } elseif ( $wpsso->debug->enabled ) {
3133
3134 $wpsso->debug->log_arr( 'missing minValue and maxValue', $quantity );
3135 }
3136 }
3137
3138 if ( ! empty( $delivery_time ) ) {
3139
3140 $json_data[ 'deliveryTime' ] = WpssoSchema::get_schema_type_context(
3141 'https://schema.org/ShippingDeliveryTime', $delivery_time );
3142 }
3143 }
3144
3145 if ( ! empty( $ship_offer[ 'shipping_id' ] ) ) {
3146
3147 if ( ! empty( $ship_offer[ 'shipping_rel' ] ) ) {
3148
3149 WpssoSchema::update_data_id( $json_data, $ship_offer[ 'shipping_id' ], $ship_offer[ 'shipping_rel' ] );
3150
3151 } else WpssoSchema::update_data_id( $json_data, $ship_offer[ 'shipping_id' ], $offer_url );
3152 }
3153
3154 /*
3155 * Filter the single Shipping Offer data.
3156 */
3157 if ( $wpsso->debug->enabled ) {
3158
3159 $wpsso->debug->log( 'applying filters "wpsso_json_data_single_shipping_offer"' );
3160 }
3161
3162 $json_data = apply_filters( 'wpsso_json_data_single_shipping_offer', $json_data, $mod );
3163
3164 return $json_data;
3165 }
3166
3167 /*
3168 * Returns an array or false if there are no open/close hours.
3169 *
3170 * Example $opts = Array (
3171 * [shipdept_rel] => http://adm.surniaulula.com/produit/a-variable-product/
3172 * [shipdept_timezone] => America/Vancouver
3173 * [shipdept_midday_close] => 12:00
3174 * [shipdept_midday_open] => 13:00
3175 * [shipdept_cutoff] => 16:00
3176 * [shipdept_day_sunday_open] => none
3177 * [shipdept_day_sunday_close] => none
3178 * [shipdept_day_monday_open] => 09:00
3179 * [shipdept_day_monday_close] => 17:00
3180 * [shipdept_day_tuesday_open] => 09:00
3181 * [shipdept_day_tuesday_close] => 17:00
3182 * [shipdept_day_wednesday_open] => 09:00
3183 * [shipdept_day_wednesday_close] => 17:00
3184 * [shipdept_day_thursday_open] => 09:00
3185 * [shipdept_day_thursday_close] => 17:00
3186 * [shipdept_day_friday_open] => 09:00
3187 * [shipdept_day_friday_close] => 17:00
3188 * [shipdept_day_saturday_open] => none
3189 * [shipdept_day_saturday_close] => none
3190 * [shipdept_day_publicholidays_open] => 09:00
3191 * [shipdept_day_publicholidays_close] => 12:00
3192 * )
3193 */
3194 public static function get_opening_hours_data( array $opts, $opt_prefix = 'place' ) {
3195
3196 $wpsso =& Wpsso::get_instance();
3197
3198 $hours_rel = isset( $opts[ $opt_prefix . '_rel' ] ) ? $opts[ $opt_prefix . '_rel' ] : '';
3199 $opening_hours_spec = array();
3200
3201 foreach ( $wpsso->cf[ 'form' ][ 'weekdays' ] as $day_key => $day_label ) {
3202
3203 /*
3204 * Returns an empty array or an associative array of open => close hours, including a timezone offset.
3205 *
3206 * $open_close = Array (
3207 * [08:00-07:00] => 17:00-07:00
3208 * )
3209 *
3210 * -07:00 is a timezone offset.
3211 */
3212 $open_close = self::get_open_close_pairs(
3213 $opts,
3214 $opt_prefix . '_day_' . $day_key . '_open',
3215 $opt_prefix . '_midday_close',
3216 $opt_prefix . '_midday_open',
3217 $opt_prefix . '_day_' . $day_key . '_close',
3218 $opt_prefix . '_timezone'
3219 );
3220
3221 if ( ! empty( $open_close ) ) {
3222
3223 foreach ( $open_close as $open => $close ) {
3224
3225 $weekday_spec = array(
3226 '@context' => 'https://schema.org',
3227 '@type' => 'OpeningHoursSpecification',
3228 'dayOfWeek' => $wpsso->cf[ 'form' ][ 'day_of_week' ][ $day_key ],
3229 'opens' => $open,
3230 'closes' => $close,
3231 );
3232
3233 foreach ( array(
3234 'validFrom' => $opt_prefix . '_season_from_date',
3235 'validThrough' => $opt_prefix . '_season_to_date',
3236 ) as $prop_name => $opt_key ) {
3237
3238 if ( isset( $opts[ $opt_key ] ) && '' !== $opts[ $opt_key ] ) {
3239
3240 $weekday_spec[ $prop_name ] = $opts[ $opt_key ];
3241 }
3242 }
3243
3244 $hours_id = array( 'hours', md5( wp_json_encode( $weekday_spec ) ) );
3245
3246 WpssoSchema::update_data_id( $weekday_spec, $hours_id, $hours_rel );
3247
3248 $opening_hours_spec[] = $weekday_spec;
3249 }
3250 }
3251 }
3252
3253 return empty( $opening_hours_spec ) ? false : $opening_hours_spec;
3254 }
3255
3256 /*
3257 * Returns an empty array or an associative array of open => close hours, including a timezone offset.
3258 *
3259 * $open_close = Array (
3260 * [08:00:00-07:00] => 17:00:00-07:00
3261 * )
3262 *
3263 * Note that "-07:00" is a timezone offset, not a range. :)
3264 *
3265 * See WpssoSchemaSingle::get_opening_hours_data().
3266 */
3267 private static function get_open_close_pairs( array $opts, $key_day_o, $key_midday_c, $key_midday_o, $key_day_c, $key_tz = '' ) {
3268
3269 $open_close_pairs = array();
3270 $is_valid_open_close = false;
3271 $is_valid_midday = false;
3272
3273 if ( isset( $opts[ $key_day_o ] ) && isset( $opts[ $key_day_c ] ) ) {
3274
3275 $is_valid_open_close = self::is_valid_open_close( $opts[ $key_day_o ], $opts[ $key_day_c ] );
3276
3277 if ( isset( $opts[ $key_midday_c ] ) && isset( $opts[ $key_midday_o ] ) ) {
3278
3279 $is_valid_midday = self::is_valid_midday( $opts[ $key_midday_c ], $opts[ $key_midday_o ] );
3280 }
3281
3282 if ( $is_valid_open_close ) {
3283
3284 $timezone = empty( $key_tz ) || empty( $opts[ $key_tz ] ) ? SucomUtil::get_default_timezone() : $opts[ $key_tz ];
3285 $tz_offset = SucomUtil::get_timezone_offset_hours( $timezone );
3286 $hm_tz_o = $opts[ $key_day_o ] . ':00' . $tz_offset;
3287 $hm_tz_c = $opts[ $key_day_c ] . ':00' . $tz_offset;
3288
3289 if ( $is_valid_midday ) {
3290
3291 $hm_tz_midday_c = $opts[ $key_midday_c ] . ':00' . $tz_offset;
3292 $hm_tz_midday_o = $opts[ $key_midday_o ] . ':00' . $tz_offset;
3293
3294 $open_close_pairs[ $hm_tz_o ] = $hm_tz_midday_c;
3295 $open_close_pairs[ $hm_tz_midday_o ] = $hm_tz_c;
3296
3297 } else $open_close_pairs[ $hm_tz_o ] = $hm_tz_c;
3298 }
3299 }
3300
3301 return $open_close_pairs;
3302 }
3303
3304 private static function is_valid_open_close( $hm_o, $hm_c ) {
3305
3306 if ( empty( $hm_o ) || empty( $hm_c ) || 'none' === $hm_o || 'none' === $hm_c ) {
3307
3308 return false;
3309 }
3310
3311 return true;
3312 }
3313
3314 private static function is_valid_midday( $hm_midday_c, $hm_midday_o ) {
3315
3316 if ( empty( $hm_midday_c ) || empty( $hm_midday_o ) || 'none' === $hm_midday_c || 'none' === $hm_midday_o ) {
3317
3318 return false;
3319 }
3320
3321 $hm_midday_c_time = strtotime( $hm_midday_c );
3322 $hm_midday_o_time = strtotime( $hm_midday_o );
3323
3324 if ( $hm_midday_c_time < $hm_midday_o_time ) {
3325
3326 return true;
3327 }
3328
3329 return false;
3330 }
3331
3332 /*
3333 * If not adding a list element, then get the existing schema type url (if one exists).
3334 */
3335 private static function get_data_types( $json_data, $type_opts, $opt_key, $def_type_id, $list_el = false ) {
3336
3337 $wpsso =& Wpsso::get_instance();
3338
3339 $single_type_id = false;
3340 $single_type_url = false;
3341 $single_type_from = 'inherited';
3342
3343 if ( ! $list_el || 'merge' === $list_el ) {
3344
3345 $single_type_url = WpssoSchema::get_data_type_url( $json_data );
3346 }
3347
3348 if ( ! $single_type_url ) {
3349
3350 /*
3351 * $type_opts may be false, null, or an array.
3352 */
3353 if ( empty( $type_opts[ $opt_key ] ) || 'none' === $type_opts[ $opt_key ] ) {
3354
3355 $single_type_id = $def_type_id;
3356 $single_type_url = $wpsso->schema->get_schema_type_url( $def_type_id );
3357 $single_type_from = 'default';
3358
3359 } else {
3360
3361 $single_type_id = $type_opts[ $opt_key ];
3362 $single_type_url = $wpsso->schema->get_schema_type_url( $single_type_id, $def_type_id );
3363 $single_type_from = 'options';
3364 }
3365
3366 } else $single_type_id = $wpsso->schema->get_schema_type_url_id( $single_type_url, $def_type_id );
3367
3368 if ( $wpsso->debug->enabled ) {
3369
3370 $wpsso->debug->log( 'using ' . $single_type_from . ' single type url: ' . $single_type_url );
3371 }
3372
3373 return array( $single_type_id, $single_type_url );
3374 }
3375
3376 private static function add_or_replace_data( &$json_data, array $json_ret, $list_el ) {
3377
3378 if ( empty( $list_el ) ) { // Add a single item.
3379
3380 $json_data = $json_ret;
3381
3382 } elseif ( 'merge' === $list_el ) {
3383
3384 $json_data = is_array( $json_data ) ? array_merge( $json_data, $json_ret ) : $json_ret;
3385
3386 } elseif ( is_array( $json_data ) ) { // Just in case.
3387
3388 if ( SucomUtil::is_assoc( $json_data ) ) { // Converting from associative to array element.
3389
3390 $json_data = array( $json_data );
3391 }
3392
3393 $json_data[] = $json_ret; // Add an item to the list.
3394
3395 } else $json_data = array( $json_ret ); // Add an item to the list.
3396 }
3397 }
3398 }
3399