PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / 22.7.0
WPSSO Core – Complete Schema Markup and Meta Tags v22.7.0
22.7.0 22.6.1 22.6.0 22.5.3 22.5.2 22.5.1 22.4.0 22.3.0 22.2.1 22.2.0 22.1.3 22.1.2 22.1.1 22.1.0 22.0.0 21.13.3 21.13.2 trunk 21.13.1
← All changes | lib/twittercard.php +636 -0 22.6.022.7.0 View file →
@@ -1,0 +1,636 @@
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( 'WpssoTwitterCard' ) ) {
19 +
20 + class WpssoTwitterCard {
21 +
22 + private $p; // Wpsso class object.
23 +
24 + public function __construct( &$plugin ) {
25 +
26 + $this->p =& $plugin;
27 +
28 + if ( $this->p->debug->enabled ) {
29 +
30 + $this->p->debug->mark();
31 + }
32 +
33 + $this->p->util->add_plugin_filters( $this, array(
34 + 'plugin_image_sizes' => 1,
35 + ) );
36 + }
37 +
38 + public function filter_plugin_image_sizes( array $sizes ) {
39 +
40 + $sizes[ 'tc_sum' ] = array( // Option prefix.
41 + 'name' => 'tc-summary',
42 + 'label_transl' => _x( 'X (Twitter) Summary Card', 'option label', 'wpsso' ),
43 + );
44 +
45 + $sizes[ 'tc_lrg' ] = array( // Option prefix.
46 + 'name' => 'tc-lrgimg',
47 + 'label_transl' => _x( 'X (Twitter) Summary Card Large Image', 'option label', 'wpsso' ),
48 + );
49 +
50 + return $sizes;
51 + }
52 +
53 + /*
54 + * Use reference for $mt_og argument to allow unset of existing twitter meta tags.
55 + */
56 + public function get_array( array $mod, array $mt_og = array(), $author_id = false ) {
57 +
58 + if ( $this->p->debug->enabled ) {
59 +
60 + $this->p->debug->mark();
61 + }
62 +
63 + $mt_tc = apply_filters( 'wpsso_tc_seed', array(), $mod );
64 +
65 + /*
66 + * The twitter:domain is used in place of the 'view on web' text.
67 + */
68 + if ( ! isset( $mt_tc[ 'twitter:domain' ] ) && ! empty( $mt_og[ 'og:url' ] ) ) {
69 +
70 + $mt_tc[ 'twitter:domain' ] = preg_replace( '/^.*\/\/([^\/]+).*$/', '$1', $mt_og[ 'og:url' ] );
71 + }
72 +
73 + if ( ! isset( $mt_tc[ 'twitter:site' ] ) ) {
74 +
75 + $mt_tc[ 'twitter:site' ] = SucomUtilOptions::get_key_value( 'tc_site', $this->p->options, $mod );
76 + }
77 +
78 + if ( ! isset( $mt_tc[ 'twitter:title' ] ) ) {
79 +
80 + $mt_tc[ 'twitter:title' ] = $this->p->page->get_title( $mod, $md_key = 'tc_title', $max_len = 'tc_title' );
81 + }
82 +
83 + if ( ! isset( $mt_tc[ 'twitter:description' ] ) ) {
84 +
85 + $mt_tc[ 'twitter:description' ] = $this->p->page->get_description( $mod, $md_key = 'tc_desc', $max_len = 'tc_desc', $num_hashtags = true );
86 + }
87 +
88 + if ( ! isset( $mt_tc[ 'twitter:creator' ] ) ) {
89 +
90 + if ( $author_id ) {
91 +
92 + $mt_tc[ 'twitter:creator' ] = get_the_author_meta( $this->p->options[ 'plugin_cm_twitter_name' ], $author_id );
93 +
94 + $mt_tc[ 'twitter:creator' ] = SucomUtil::sanitize_twitter_name( $mt_tc[ 'twitter:creator' ] ); // Just in case.
95 + }
96 + }
97 +
98 + /*
99 + * Player video card.
100 + */
101 + $this->maybe_add_player_card( $mt_tc, $mod, $mt_og, $author_id );
102 +
103 + /*
104 + * Post summary or large image summary card.
105 + */
106 + $this->maybe_add_post_card( $mt_tc, $mod, $mt_og, $author_id );
107 +
108 + /*
109 + * Default card.
110 + */
111 + $this->maybe_add_default_card( $mt_tc, $mod, $mt_og, $author_id );
112 +
113 + /*
114 + * Additional article and product data.
115 + */
116 + $this->maybe_add_extra_data( $mt_tc, $mod, $mt_og, $author_id );
117 +
118 + return apply_filters( 'wpsso_tc', $mt_tc, $mod );
119 + }
120 +
121 + /*
122 + * $mixed = 'singular' | 'default' | $mod.
123 + *
124 + * Example return:
125 + *
126 + * array(
127 + * 'summary_large_image',
128 + * 'X (Twitter) Summary Card Large Image',
129 + * 'wpsso-tc-lrgimg',
130 + * 'tc_lrg',
131 + * )
132 + */
133 + public function get_card_info( $mixed, array $head_tags = array(), $use_md_opts = true ) {
134 +
135 + if ( $this->p->debug->enabled ) {
136 +
137 + $this->p->debug->mark();
138 + }
139 +
140 + $card_type = 'summary';
141 + $size_label = '';
142 + $size_name = 'thumbnail'; // Just in case.
143 + $md_pre = '';
144 +
145 + if ( ! empty( $head_tags[ 'twitter:card' ] ) ) {
146 +
147 + $card_type = $head_tags[ 'twitter:card' ];
148 +
149 + } elseif ( is_string( $mixed ) ) { // $mixed is 'singular' or 'default'.
150 +
151 + if ( ! empty( $this->p->options[ 'tc_type_' . $mixed ] ) ) {
152 +
153 + $card_type = $this->p->options[ 'tc_type_' . $mixed ];
154 + }
155 +
156 + } elseif ( is_array( $mixed ) ) { // $mixed is $mod.
157 +
158 + /*
159 + * Get the default card type for this post type.
160 + */
161 + if ( ! empty( $mixed[ 'is_post' ] ) ) {
162 +
163 + if ( ! empty( $mixed[ 'post_type' ] ) && is_string( $mixed[ 'post_type' ] ) &&
164 + ! empty( $this->p->options[ 'tc_type_for_' . $mixed[ 'post_type' ] ] ) ) {
165 +
166 + $card_type = $this->p->options[ 'tc_type_for_' . $mixed[ 'post_type' ] ];
167 +
168 + } elseif ( ! empty( $this->p->options[ 'tc_type_singular' ] ) ) {
169 +
170 + $card_type = $this->p->options[ 'tc_type_singular' ];
171 + }
172 + }
173 +
174 + /*
175 + * Maybe override the default with a custom card type.
176 + */
177 + if ( ! empty( $mixed[ 'obj' ] ) && $use_md_opts ) {
178 +
179 + $md_card_type = $mixed[ 'obj' ]->get_options( $mixed[ 'id' ], 'tc_type' ); // Returns null if index key not found.
180 +
181 + if ( ! empty( $md_card_type ) ) {
182 +
183 + $card_type = $md_card_type;
184 + }
185 + }
186 + }
187 +
188 + switch ( $card_type ) {
189 +
190 + case 'app':
191 +
192 + $card_label = _x( 'X (Twitter) App Card', 'metabox title', 'wpsso' );
193 + $size_name = '';
194 + $md_pre = 'tc_app';
195 +
196 + break;
197 +
198 + case 'player':
199 +
200 + $card_label = _x( 'X (Twitter) Player Card', 'metabox title', 'wpsso' );
201 + $size_name = '';
202 + $md_pre = 'tc_play';
203 +
204 + break;
205 +
206 + case 'summary':
207 +
208 + $card_label = _x( 'X (Twitter) Summary Card', 'metabox title', 'wpsso' );
209 + $size_name = 'wpsso-tc-summary';
210 + $md_pre = 'tc_sum';
211 +
212 + break;
213 +
214 + case 'summary_large_image':
215 +
216 + $card_label = _x( 'X (Twitter) Summary Card Large Image', 'metabox title', 'wpsso' );
217 + $size_name = 'wpsso-tc-lrgimg';
218 + $md_pre = 'tc_lrg';
219 +
220 + break;
221 + }
222 +
223 + /*
224 + * Example return:
225 + *
226 + * array(
227 + * 'summary_large_image',
228 + * 'X (Twitter) Summary Card Large Image',
229 + * 'wpsso-tc-lrgimg',
230 + * 'tc_lrg',
231 + * )
232 + */
233 + $card_info = array( $card_type, $card_label, $size_name, $md_pre );
234 +
235 + if ( $this->p->debug->enabled ) {
236 +
237 + $this->p->debug->log_arr( 'card_info', $card_info );
238 + }
239 +
240 + return $card_info;
241 + }
242 +
243 + /*
244 + * Player video card.
245 + *
246 + * The twitter:player:stream meta tags are used for self-hosted MP4 videos. The videos provided by
247 + * YouTube, Vimeo, Wistia, etc. are application/x-shockwave-flash or text/html.
248 + *
249 + * twitter:player:stream
250 + * This is a URL to the video file itself (not a video embed). The video must be an mp4 file. The
251 + * supported codecs within the file are: H.264 video, Baseline Profile Level 3.0, up to 640 x 480 at
252 + * 30 fps and AAC Low Complexity Profile (LC) audio. This property is optional.
253 + *
254 + * twitter:player:stream:content_type
255 + * The MIME type for your video file (video/mp4). This property is only required if you have set a
256 + * twitter:player:stream meta tag.
257 + */
258 + private function maybe_add_player_card( &$mt_tc, $mod, $mt_og, $author_id ) { // Pass by reference is OK.
259 +
260 + if ( $this->p->debug->enabled ) {
261 +
262 + $this->p->debug->mark();
263 + }
264 +
265 + if ( isset( $mt_tc[ 'twitter:card' ] ) ) {
266 +
267 + if ( $this->p->debug->enabled ) {
268 +
269 + $this->p->debug->log( 'exiting early: twitter card is set' );
270 + }
271 +
272 + return;
273 + }
274 +
275 + if ( empty( $mt_og[ 'og:video' ] ) || ! count( $mt_og[ 'og:video' ] ) > 1 ) {
276 +
277 + if ( $this->p->debug->enabled ) {
278 +
279 + $this->p->debug->log( 'exiting early: no videos for player card' );
280 + }
281 +
282 + return;
283 + }
284 +
285 + foreach ( $mt_og[ 'og:video' ] as $mt_single_video ) {
286 +
287 + $player_embed_url = '';
288 + $player_stream_url = '';
289 +
290 + /*
291 + * Check for internal meta tag embed_url or stream_url.
292 + */
293 + if ( ! empty( $mt_single_video[ 'og:video:embed_url' ] ) ) {
294 +
295 + $player_embed_url = $mt_single_video[ 'og:video:embed_url' ];
296 +
297 + if ( $this->p->debug->enabled ) {
298 +
299 + $this->p->debug->log( 'player card: embed url = ' . $player_embed_url );
300 + }
301 + }
302 +
303 + if ( ! empty( $mt_single_video[ 'og:video:stream_url' ] ) ) {
304 +
305 + $player_stream_url = $mt_single_video[ 'og:video:stream_url' ];
306 +
307 + if ( $this->p->debug->enabled ) {
308 +
309 + $this->p->debug->log( 'player card: stream url = ' . $player_stream_url );
310 + }
311 + }
312 +
313 + /*
314 + * Check for a video mime-type meta tag.
315 + */
316 + if ( isset( $mt_single_video[ 'og:video:type' ] ) ) {
317 +
318 + switch ( $mt_single_video[ 'og:video:type' ] ) {
319 +
320 + /*
321 + * twitter:player
322 + *
323 + * HTTPS URL to iFrame player. This must be a HTTPS URL which does not generate active
324 + * mixed content warnings in a web browser. The audio or video player must not require
325 + * plugins such as Adobe Flash.
326 + */
327 + case 'text/html':
328 +
329 + if ( empty( $player_embed_url ) ) {
330 +
331 + $player_embed_url = SucomUtil::get_first_mt_media_url( $mt_single_video, $media_pre = 'og:video' );
332 +
333 + if ( $this->p->debug->enabled ) {
334 +
335 + $this->p->debug->log( 'player card: ' . $mt_single_video[ 'og:video:type' ] .
336 + ' url = ' . $player_embed_url );
337 + }
338 + }
339 +
340 + break;
341 +
342 + /*
343 + * twitter:player:stream
344 + */
345 + case 'video/mp4':
346 +
347 + if ( empty( $player_stream_url ) ) {
348 +
349 + $player_stream_url = SucomUtil::get_first_mt_media_url( $mt_single_video, $media_pre = 'og:video' );
350 +
351 + if ( $this->p->debug->enabled ) {
352 +
353 + $this->p->debug->log( 'player card: ' . $mt_single_video[ 'og:video:type' ] .
354 + ' url = ' . $player_stream_url );
355 + }
356 + }
357 +
358 + break;
359 +
360 + default:
361 +
362 + if ( $this->p->debug->enabled ) {
363 +
364 + $this->p->debug->log( 'player card: video type "' . $mt_single_video[ 'og:video:type' ] . '" is unknown' );
365 + }
366 +
367 + break;
368 + }
369 + }
370 +
371 + /*
372 + * Set the twitter:player meta tag value(s).
373 + */
374 + if ( ! empty( $player_embed_url ) ) {
375 +
376 + $mt_tc[ 'twitter:card' ] = 'player';
377 + $mt_tc[ 'twitter:player' ] = $player_embed_url;
378 + }
379 +
380 + if ( ! empty( $player_stream_url ) ) {
381 +
382 + $mt_tc[ 'twitter:card' ] = 'player';
383 +
384 + if ( empty( $mt_tc[ 'twitter:player' ] ) ) {
385 +
386 + $mt_tc[ 'twitter:player' ] = $player_stream_url; // Fallback to video/mp4.
387 + }
388 +
389 + $mt_tc[ 'twitter:player:stream' ] = $player_stream_url;
390 + $mt_tc[ 'twitter:player:stream:content_type' ] = $mt_single_video[ 'og:video:type' ];
391 + }
392 +
393 + /*
394 + * Set twitter:player related values (player width, height, mobile apps, etc.)
395 + */
396 + if ( ! empty( $mt_tc[ 'twitter:card' ] ) ) {
397 +
398 + foreach ( array(
399 + 'og:video:width' => 'twitter:player:width',
400 + 'og:video:height' => 'twitter:player:height',
401 + 'og:video:iphone_name' => 'twitter:app:name:iphone',
402 + 'og:video:iphone_id' => 'twitter:app:id:iphone',
403 + 'og:video:iphone_url' => 'twitter:app:url:iphone',
404 + 'og:video:ipad_name' => 'twitter:app:name:ipad',
405 + 'og:video:ipad_id' => 'twitter:app:id:ipad',
406 + 'og:video:ipad_url' => 'twitter:app:url:ipad',
407 + 'og:video:googleplay_name' => 'twitter:app:name:googleplay',
408 + 'og:video:googleplay_id' => 'twitter:app:id:googleplay',
409 + 'og:video:googleplay_url' => 'twitter:app:url:googleplay',
410 + ) as $og_name => $tc_name ) {
411 +
412 + if ( ! empty( $mt_single_video[ $og_name ] ) ) {
413 +
414 + $mt_tc[ $tc_name ] = $mt_single_video[ $og_name ];
415 + }
416 + }
417 +
418 + /*
419 + * Get the video preview image (if one is available).
420 + */
421 + $mt_tc[ 'twitter:image' ] = SucomUtil::get_first_mt_media_url( $mt_single_video );
422 +
423 + if ( ! empty( $mt_single_video[ 'og:image:alt' ] ) ) {
424 +
425 + $mt_tc[ 'twitter:image:alt' ] = $mt_single_video[ 'og:image:alt' ];
426 + }
427 +
428 + /*
429 + * Fallback to the open graph image.
430 + */
431 + if ( empty( $mt_tc[ 'twitter:image' ] ) && ! empty( $mt_og[ 'og:image' ] ) ) {
432 +
433 + if ( $this->p->debug->enabled ) {
434 +
435 + $this->p->debug->log( 'player card: no video image - using og:image instead' );
436 + }
437 +
438 + $mt_tc[ 'twitter:image' ] = SucomUtil::get_first_mt_media_url( $mt_og[ 'og:image' ] );
439 + }
440 + }
441 +
442 + return; // Use only the first video.
443 + }
444 + }
445 +
446 + /*
447 + * Post summary or large image summary card.
448 + */
449 + private function maybe_add_post_card( &$mt_tc, $mod, $mt_og, $author_id ) { // Pass by reference is OK.
450 +
451 + if ( $this->p->debug->enabled ) {
452 +
453 + $this->p->debug->mark();
454 + }
455 +
456 + if ( isset( $mt_tc[ 'twitter:card' ] ) ) {
457 +
458 + if ( $this->p->debug->enabled ) {
459 +
460 + $this->p->debug->log( 'exiting early: twitter card is set' );
461 + }
462 +
463 + return;
464 + }
465 +
466 + if ( empty( $mod[ 'is_post' ] ) ) {
467 +
468 + if ( $this->p->debug->enabled ) {
469 +
470 + $this->p->debug->log( 'exiting early: module is not post object' );
471 + }
472 +
473 + return;
474 + }
475 +
476 + list( $card_type, $card_label, $size_name, $md_pre ) = $this->get_card_info( $mod );
477 +
478 + /*
479 + * Post image.
480 + */
481 + if ( $this->p->debug->enabled ) {
482 +
483 + $this->p->debug->log( $card_type . ' card: checking for post image (meta, featured, attached)' );
484 + }
485 +
486 + $mt_images = $this->p->media->get_post_images( 1, $size_name, $mod[ 'id' ], $md_pre );
487 +
488 + if ( count( $mt_images ) > 0 ) {
489 +
490 + $mt_single_image = reset( $mt_images );
491 +
492 + $image_url = SucomUtil::get_first_mt_media_url( $mt_single_image );
493 +
494 + /*
495 + * 'summary_large_image' webpages cannot have the same image URLs, so add the post ID to all
496 + * 'summary_large_image' images.
497 + */
498 + if ( 'summary_large_image' === $card_type ) {
499 +
500 + $image_url = add_query_arg( 'p', $mod[ 'id' ], $image_url );
501 + }
502 +
503 + $mt_tc[ 'twitter:card' ] = $card_type;
504 + $mt_tc[ 'twitter:image' ] = $image_url;
505 +
506 + if ( ! empty( $mt_single_image[ 'og:image:alt' ] ) ) {
507 +
508 + $mt_tc[ 'twitter:image:alt' ] = $mt_single_image[ 'og:image:alt' ];
509 + }
510 +
511 + } elseif ( $this->p->debug->enabled ) {
512 +
513 + $this->p->debug->log( 'no post image found' );
514 + }
515 + }
516 +
517 + /*
518 + * Default card.
519 + */
520 + private function maybe_add_default_card( &$mt_tc, $mod, $mt_og, $author_id ) { // Pass by reference is OK.
521 +
522 + if ( $this->p->debug->enabled ) {
523 +
524 + $this->p->debug->mark();
525 + }
526 +
527 + if ( isset( $mt_tc[ 'twitter:card' ] ) ) {
528 +
529 + if ( $this->p->debug->enabled ) {
530 +
531 + $this->p->debug->log( 'exiting early: twitter card is set' );
532 + }
533 +
534 + return;
535 + }
536 +
537 + list( $card_type, $card_label, $size_name, $md_pre ) = $this->get_card_info( 'default' );
538 +
539 + if ( $this->p->debug->enabled ) {
540 +
541 + $this->p->debug->log( $card_type . ' card: using default card type' );
542 + }
543 +
544 + $mt_tc[ 'twitter:card' ] = $card_type;
545 +
546 + if ( $this->p->debug->enabled ) {
547 +
548 + $this->p->debug->log( $card_type . ' card: checking other images' );
549 + }
550 +
551 + $mt_images = $this->p->media->get_all_images( $num = 1, $size_name, $mod, $md_pre );
552 +
553 + if ( count( $mt_images ) > 0 ) {
554 +
555 + $mt_single_image = reset( $mt_images );
556 +
557 + $image_url = SucomUtil::get_first_mt_media_url( $mt_single_image );
558 +
559 + $mt_tc[ 'twitter:image' ] = $image_url;
560 +
561 + if ( ! empty( $mt_single_image[ 'og:image:alt' ] ) ) {
562 +
563 + $mt_tc[ 'twitter:image:alt' ] = $mt_single_image[ 'og:image:alt' ];
564 + }
565 +
566 + } elseif ( $this->p->debug->enabled ) {
567 +
568 + $this->p->debug->log( 'no other images found' );
569 + }
570 + }
571 +
572 + /*
573 + * Additional article and product data.
574 + */
575 + private function maybe_add_extra_data( &$mt_tc, $mod, $mt_og, $author_id ) { // Pass by reference is OK.
576 +
577 + if ( $this->p->debug->enabled ) {
578 +
579 + $this->p->debug->mark();
580 + }
581 +
582 + if ( isset( $mt_og[ 'og:type' ] ) ) {
583 +
584 + switch ( $mt_og[ 'og:type' ] ) {
585 +
586 + case 'article':
587 +
588 + if ( ! empty( $mt_og[ 'article:author:name' ] ) ) {
589 +
590 + $mt_tc[ 'twitter:label1' ] = __( 'Written by', 'wpsso' );
591 +
592 + $mt_tc[ 'twitter:data1' ] = $mt_og[ 'article:author:name' ];
593 + }
594 +
595 + if ( ! empty( $mt_og[ 'article:reading_mins' ] ) ) {
596 +
597 + $mt_tc[ 'twitter:label2' ] = __( 'Est. reading time', 'wpsso' );
598 +
599 + $mt_tc[ 'twitter:data2' ] = $this->p->page->fmt_reading_mins( $mt_og[ 'article:reading_mins' ] );
600 + }
601 +
602 + break;
603 +
604 + case 'product':
605 +
606 + if ( isset( $mt_og[ 'product:price:amount' ] ) && isset( $mt_og[ 'product:price:currency' ] ) ) {
607 +
608 + $mt_tc[ 'twitter:label1' ] = __( 'Price', 'wpsso' );
609 +
610 + $mt_tc[ 'twitter:data1' ] = $mt_og[ 'product:price:amount' ] . ' ' . $mt_og[ 'product:price:currency' ];
611 + }
612 +
613 + if ( isset( $mt_og[ 'product:availability' ] ) ) {
614 +
615 + $mt_name = 'product:availability';
616 + $map_key = $mt_og[ $mt_name ];
617 +
618 + /*
619 + * Map 'https://schema.org/InStock' to 'in stock', for example.
620 + */
621 + if ( ! empty( $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ][ $map_key ] ) ) {
622 +
623 + $map_key = $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ][ $map_key ];
624 + }
625 +
626 + $mt_tc[ 'twitter:label2' ] = __( 'Availability', 'wpsso' );
627 +
628 + $mt_tc[ 'twitter:data2' ] = ucwords( $map_key );
629 + }
630 +
631 + break;
632 + }
633 + }
634 + }
635 + }
636 +}