PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-post-media / src / class-twitter-cards.php

class-twitter-cards.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-post-media/src/class-twitter-cards.php

344 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Twitter Cards handling.
4 *
5 * @package automattic/jetpack-post-media
6 */
7
8 namespace Automattic\Jetpack\Post_Media;
9
10 use WP_Post;
11
12 /**
13 * Twitter Cards
14 *
15 * Hooks onto the Open Graph protocol and extends it by adding only the tags
16 * we need for twitter cards.
17 *
18 * @see https://dev.twitter.com/cards/overview
19 */
20 class Twitter_Cards {
21
22 /**
23 * Adds Twitter Card tags.
24 *
25 * @param array $og_tags Existing OG tags.
26 *
27 * @return array OG tags inclusive of Twitter Card output.
28 */
29 public static function twitter_cards_tags( $og_tags ) {
30 global $post;
31 $post_id = ( $post instanceof WP_Post ) ? $post->ID : null;
32
33 /**
34 * Maximum alt text length.
35 *
36 * @see https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary-card-with-large-image.html
37 */
38 $alt_length = 420;
39
40 if ( post_password_required() ) {
41 return $og_tags;
42 }
43
44 /** This action is documented in class.jetpack.php */
45 if ( apply_filters( 'jetpack_disable_twitter_cards', false ) ) {
46 return $og_tags;
47 }
48
49 /*
50 * These tags apply to any page (home, archives, etc).
51 */
52
53 // If we have information on the author/creator, then include that as well.
54 if ( ! empty( $post ) && ! empty( $post->post_author ) ) {
55 /** This action is documented in modules/sharedaddy/sharing-sources.php */
56 $handle = apply_filters( 'jetpack_sharing_twitter_via', '', $post_id );
57 if ( ! empty( $handle ) && ! self::is_default_site_tag( $handle ) ) {
58 $og_tags['twitter:creator'] = self::sanitize_twitter_user( $handle );
59 }
60 }
61
62 $site_tag = self::site_tag();
63 /** This action is documented in modules/sharedaddy/sharing-sources.php */
64 $site_tag = apply_filters( 'jetpack_sharing_twitter_via', $site_tag, ( is_singular() ? $post_id : null ) );
65 /** This action is documented in modules/sharedaddy/sharing-sources.php */
66 $site_tag = apply_filters( 'jetpack_twitter_cards_site_tag', $site_tag, $og_tags );
67 if ( ! empty( $site_tag ) ) {
68 $og_tags['twitter:site'] = self::sanitize_twitter_user( $site_tag );
69 }
70
71 if ( ! is_singular() || ! empty( $og_tags['twitter:card'] ) ) {
72 /**
73 * Filter the default Twitter card image, used when no image can be found in a post.
74 *
75 * @module sharedaddy
76 *
77 * @since jetpack-5.9.0
78 *
79 * @param string $str Default image URL.
80 */
81 $image = apply_filters( 'jetpack_twitter_cards_image_default', '' );
82 if ( ! empty( $image ) ) {
83 $og_tags['twitter:image'] = $image;
84 }
85
86 return $og_tags;
87 }
88
89 $the_title = get_the_title();
90 if ( ! $the_title ) {
91 $the_title = get_bloginfo( 'name' );
92 }
93 $og_tags['twitter:text:title'] = $the_title;
94
95 /*
96 * The following tags only apply to single pages.
97 */
98
99 $card_type = 'summary';
100
101 // Try to give priority to featured images.
102 if ( ! empty( $post_id ) ) {
103 $post_image = Images::get_image(
104 $post_id,
105 array(
106 'width' => 144,
107 'height' => 144,
108 )
109 );
110 if ( ! empty( $post_image ) && is_array( $post_image ) ) {
111 // 4096 is the maximum size for an image per https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary .
112 if (
113 isset( $post_image['src_width'] ) && isset( $post_image['src_height'] )
114 && (int) $post_image['src_width'] <= 4096
115 && (int) $post_image['src_height'] <= 4096
116 ) {
117 // 300x157 is the minimum size for a summary_large_image per https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary-card-with-large-image .
118 if ( (int) $post_image['src_width'] >= 300 && (int) $post_image['src_height'] >= 157 ) {
119 $card_type = 'summary_large_image';
120 $og_tags['twitter:image'] = esc_url( add_query_arg( 'w', 640, $post_image['src'] ) );
121 } else {
122 $og_tags['twitter:image'] = esc_url( add_query_arg( 'w', 144, $post_image['src'] ) );
123 }
124
125 // Add the alt tag if we have one.
126 if ( ! empty( $post_image['alt_text'] ) ) {
127 // Shorten it if it is too long.
128 if ( strlen( $post_image['alt_text'] ) > $alt_length ) {
129 $og_tags['twitter:image:alt'] = esc_attr( mb_substr( $post_image['alt_text'], 0, $alt_length ) . '…' );
130 } else {
131 $og_tags['twitter:image:alt'] = esc_attr( $post_image['alt_text'] );
132 }
133 }
134 }
135 }
136 }
137
138 $extract = array();
139
140 // Only proceed with media analysis if a featured image has not superseded it already.
141 if ( empty( $og_tags['twitter:image'] ) && empty( $og_tags['twitter:image:src'] ) ) {
142 // @todo Jetpack_Media_Summary is defined in the Jetpack plugin. It should be moved to this package.
143 if ( ! class_exists( 'Jetpack_Media_Summary' ) && defined( 'JETPACK__PLUGIN_DIR' ) ) {
144 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-summary.php';
145 }
146
147 // Test again, class should already be auto-loaded in Jetpack.
148 // If not, skip extra media analysis and stick with a summary card.
149 if ( class_exists( 'Jetpack_Media_Summary' ) && ! empty( $post_id ) ) {
150 $extract = \Jetpack_Media_Summary::get( $post_id ); // @phan-suppress-current-line PhanUndeclaredClassMethod -- Guarded by class_exists().
151
152 if ( 'gallery' === $extract['type'] ) {
153 list( $og_tags, $card_type ) = self::twitter_cards_define_type_based_on_image_count( $og_tags, $extract );
154 } elseif ( 'video' === $extract['type'] ) {
155 // Leave as summary, but with large pict of poster frame (we know those comply to Twitter's size requirements).
156 $card_type = 'summary_large_image';
157 $og_tags['twitter:image'] = esc_url( add_query_arg( 'w', 640, $extract['image'] ) );
158 } else {
159 list( $og_tags, $card_type ) = self::twitter_cards_define_type_based_on_image_count( $og_tags, $extract );
160 }
161 }
162 }
163
164 $og_tags['twitter:card'] = $card_type;
165
166 // Make sure we have a description for Twitter, their validator isn't happy without some content (single space not valid).
167 if ( ! isset( $og_tags['og:description'] ) || '' === trim( $og_tags['og:description'] ) || __( 'Visit the post for more.', 'jetpack-post-media' ) === $og_tags['og:description'] ) { // empty( trim( $og_tags['og:description'] ) ) isn't valid php.
168 $has_creator = ! empty( $og_tags['twitter:creator'] ) && '@wordpressdotcom' !== $og_tags['twitter:creator'];
169 if ( ! empty( $extract ) && 'video' === $extract['type'] ) { // use $extract['type'] since $card_type is 'summary' for video posts.
170 /* translators: %s is the post author */
171 $og_tags['twitter:description'] = ( $has_creator ) ? sprintf( __( 'Video post by %s.', 'jetpack-post-media' ), $og_tags['twitter:creator'] ?? '' ) : __( 'Video post.', 'jetpack-post-media' );
172 } else {
173 /* translators: %s is the post author */
174 $og_tags['twitter:description'] = ( $has_creator ) ? sprintf( __( 'Post by %s.', 'jetpack-post-media' ), $og_tags['twitter:creator'] ?? '' ) : __( 'Visit the post for more.', 'jetpack-post-media' );
175 }
176 }
177
178 if ( empty( $og_tags['twitter:image'] ) && empty( $og_tags['twitter:image:src'] ) ) {
179 /** This action is documented in class-twitter-cards.php */
180 $image = apply_filters( 'jetpack_twitter_cards_image_default', '' );
181 if ( ! empty( $image ) ) {
182 $og_tags['twitter:image'] = $image;
183 }
184 }
185
186 return $og_tags;
187 }
188
189 /**
190 * Sanitize the Twitter user by normalizing the @.
191 *
192 * @param string $str Twitter user value.
193 *
194 * @return string Twitter user value.
195 */
196 public static function sanitize_twitter_user( $str ) {
197 return '@' . preg_replace( '/^@/', '', $str );
198 }
199
200 /**
201 * Determines if a site tag is one of the default WP.com/Jetpack ones.
202 *
203 * @param string $site_tag Site tag.
204 *
205 * @return bool True if the default site tag is being used.
206 */
207 public static function is_default_site_tag( $site_tag ) {
208 return in_array( $site_tag, array( '@wordpressdotcom', '@jetpack', 'wordpressdotcom', 'jetpack' ), true );
209 }
210
211 /**
212 * Give priority to the creator tag if using the default site tag.
213 *
214 * @param string $site_tag Site tag.
215 * @param array $og_tags OG tags.
216 *
217 * @return string Site tag.
218 */
219 public static function prioritize_creator_over_default_site( $site_tag, $og_tags = array() ) {
220 if ( ! empty( $og_tags['twitter:creator'] ) && self::is_default_site_tag( $site_tag ) ) {
221 return $og_tags['twitter:creator'];
222 }
223 return $site_tag;
224 }
225
226 /**
227 * Define the Twitter Card type based on image count.
228 *
229 * @param array $og_tags Existing OG tags.
230 * @param array $extract Result of the Image Extractor class.
231 *
232 * @return array
233 */
234 public static function twitter_cards_define_type_based_on_image_count( $og_tags, $extract ) {
235 $card_type = 'summary';
236 $img_count = $extract['count']['image'];
237
238 if ( empty( $img_count ) ) {
239
240 // No images, use Blavatar as a thumbnail for the summary type.
241 // @todo blavatar_domain, blavatar_exists, and blavatar_url are WordPress.com functions. They should be abstracted.
242 if ( function_exists( 'blavatar_domain' ) ) {
243 $blavatar_domain = blavatar_domain( site_url() ); // @phan-suppress-current-line PhanUndeclaredFunction -- Guarded by function_exists().
244 if ( blavatar_exists( $blavatar_domain ) ) { // @phan-suppress-current-line PhanUndeclaredFunction -- Guarded by function_exists().
245 $og_tags['twitter:image'] = blavatar_url( $blavatar_domain, 'img', 240 ); // @phan-suppress-current-line PhanUndeclaredFunction -- Guarded by function_exists().
246 }
247 }
248
249 // Second fall back, Site Logo.
250 // @todo jetpack_has_site_logo and jetpack_get_site_logo are defined in the Jetpack plugin. They should be abstracted.
251 if ( empty( $og_tags['twitter:image'] ) && ( function_exists( 'jetpack_has_site_logo' ) && jetpack_has_site_logo() ) ) { // @phan-suppress-current-line PhanUndeclaredFunction -- Guarded by function_exists().
252 $og_tags['twitter:image'] = jetpack_get_site_logo( 'url' ); // @phan-suppress-current-line PhanUndeclaredFunction -- Guarded by function_exists().
253 }
254
255 // Third fall back, Site Icon.
256 if ( empty( $og_tags['twitter:image'] ) && has_site_icon() ) {
257 $og_tags['twitter:image'] = get_site_icon_url( 240 );
258 }
259
260 // Not falling back on Gravatar, because there's no way to know if we end up with an auto-generated one.
261
262 } elseif ( 'image' === $extract['type'] || 'gallery' === $extract['type'] ) {
263 // Test for $extract['type'] to limit to image and gallery, so we don't send a potential fallback image like a Gravatar as a photo post.
264 $card_type = 'summary_large_image';
265 $og_tags['twitter:image'] = esc_url( add_query_arg( 'w', 1400, ( empty( $extract['images'] ) ) ? $extract['image'] : $extract['images'][0]['url'] ) );
266 }
267
268 return array( $og_tags, $card_type );
269 }
270
271 /**
272 * Updates the Twitter Card output.
273 *
274 * @param string $og_tag A single OG tag.
275 *
276 * @return string Result of the OG tag.
277 */
278 public static function twitter_cards_output( $og_tag ) {
279 return ( str_contains( $og_tag, 'twitter:' ) ) ? preg_replace( '/property="([^"]+)"/', 'name="\1"', $og_tag ) : $og_tag;
280 }
281
282 /**
283 * Adds settings section and field.
284 *
285 * @deprecated 0.3.0 Settings > Sharing renders the field itself.
286 */
287 public static function settings_init() {
288 _deprecated_function( __METHOD__, 'post-media-0.3.0', 'Automattic\Jetpack\Sharing_Likes\Settings\Twitter_Site_Tag::render' );
289 }
290
291 /**
292 * Add global sharing options.
293 *
294 * @deprecated 0.3.0 Settings > Sharing renders the field itself.
295 */
296 public static function sharing_global_options() {
297 _deprecated_function( __METHOD__, 'post-media-0.3.0', 'Automattic\Jetpack\Sharing_Likes\Settings\Twitter_Site_Tag::render' );
298 }
299
300 /**
301 * Get the Twitter Via tag.
302 *
303 * @return string Twitter via tag.
304 */
305 public static function site_tag() {
306 $site_tag = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ?
307 trim( get_option( 'twitter_via' ) ) :
308 ( class_exists( 'Jetpack_Options' ) ? \Jetpack_Options::get_option_and_ensure_autoload( 'jetpack-twitter-cards-site-tag', '' ) : get_option( 'jetpack-twitter-cards-site-tag', '' ) );
309 if ( empty( $site_tag ) ) {
310 /** This action is documented in modules/sharedaddy/sharing-sources.php */
311 return apply_filters( 'jetpack_sharing_twitter_via', '', null );
312 }
313 return $site_tag;
314 }
315
316 /**
317 * Output the settings field.
318 *
319 * @deprecated 0.3.0 Settings > Sharing renders the field itself.
320 */
321 public static function settings_field() {
322 _deprecated_function( __METHOD__, 'post-media-0.3.0', 'Automattic\Jetpack\Sharing_Likes\Settings\Twitter_Site_Tag::render' );
323 }
324
325 /**
326 * Validate the settings submission.
327 *
328 * @deprecated 0.3.0 Settings > Sharing saves the field itself.
329 */
330 public static function settings_validate() {
331 _deprecated_function( __METHOD__, 'post-media-0.3.0', 'Automattic\Jetpack\Sharing_Likes\Settings\Twitter_Site_Tag::save' );
332 }
333
334 /**
335 * Initiates the class by registering all WordPress hooks.
336 */
337 public static function init() {
338 add_filter( 'jetpack_open_graph_tags', array( __CLASS__, 'twitter_cards_tags' ), 11 ); // $priority=11: this should hook into jetpack_open_graph_tags after 'class.jetpack-seo.php' has done so.
339 add_filter( 'jetpack_open_graph_output', array( __CLASS__, 'twitter_cards_output' ) );
340 add_filter( 'jetpack_twitter_cards_site_tag', array( __CLASS__, 'site_tag' ), -99 );
341 add_filter( 'jetpack_twitter_cards_site_tag', array( __CLASS__, 'prioritize_creator_over_default_site' ), 99, 2 );
342 }
343 }
344