Nav apraksta

class.jetpack-twitter-cards.php 13KB

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