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 / twittercard.php

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

637 lines 16.2 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( '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 }
637