Нет описания

class.media-summary.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
  2. /**
  3. * Provides media summary of a post.
  4. *
  5. * @package automattic/jetpack
  6. */
  7. /**
  8. * Class Jetpack_Media_Summary
  9. *
  10. * Priority: embed [video] > gallery > image > text
  11. */
  12. class Jetpack_Media_Summary {
  13. /**
  14. * Media cache.
  15. *
  16. * @var array
  17. */
  18. private static $cache = array();
  19. /**
  20. * Get media summary for a post.
  21. *
  22. * @param int $post_id Post ID.
  23. * @param int $blog_id Blog ID, if applicable.
  24. * @param array $args {
  25. * Optional. An array of arguments.
  26. * @type int $max_words Maximum number of words.
  27. * @type int $max_chars Maximum number of characters.
  28. * }
  29. *
  30. * @return array|mixed|void
  31. */
  32. public static function get( $post_id, $blog_id = 0, $args = array() ) {
  33. // @todo: Use type hinting in the line above when at PHP 7.0+.
  34. $post_id = (int) $post_id;
  35. $blog_id = (int) $blog_id;
  36. $defaults = array(
  37. 'max_words' => 16,
  38. 'max_chars' => 256,
  39. );
  40. $args = wp_parse_args( $args, $defaults );
  41. $switched = false;
  42. if ( ! empty( $blog_id ) && get_current_blog_id() !== $blog_id && function_exists( 'switch_to_blog' ) ) {
  43. switch_to_blog( $blog_id );
  44. $switched = true;
  45. } else {
  46. $blog_id = get_current_blog_id();
  47. }
  48. $cache_key = "{$blog_id}_{$post_id}_{$args['max_words']}_{$args['max_chars']}";
  49. if ( isset( self::$cache[ $cache_key ] ) ) {
  50. if ( $switched ) {
  51. restore_current_blog();
  52. }
  53. return self::$cache[ $cache_key ];
  54. }
  55. if ( ! class_exists( 'Jetpack_Media_Meta_Extractor' ) ) {
  56. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
  57. jetpack_require_lib( 'class.wpcom-media-meta-extractor' );
  58. } else {
  59. jetpack_require_lib( 'class.media-extractor' );
  60. }
  61. }
  62. $post = get_post( $post_id );
  63. $permalink = get_permalink( $post_id );
  64. $return = array(
  65. 'type' => 'standard',
  66. 'permalink' => $permalink,
  67. 'image' => '',
  68. 'excerpt' => '',
  69. 'word_count' => 0,
  70. 'secure' => array(
  71. 'image' => '',
  72. ),
  73. 'count' => array(
  74. 'image' => 0,
  75. 'video' => 0,
  76. 'word' => 0,
  77. 'link' => 0,
  78. ),
  79. );
  80. if ( $post instanceof WP_Post && empty( $post->post_password ) ) {
  81. $return['excerpt'] = self::get_excerpt( $post->post_content, $post->post_excerpt, $args['max_words'], $args['max_chars'], $post );
  82. $return['count']['word'] = self::get_word_count( $post->post_content );
  83. $return['count']['word_remaining'] = self::get_word_remaining_count( $post->post_content, $return['excerpt'] );
  84. $return['count']['link'] = self::get_link_count( $post->post_content );
  85. }
  86. $extract = Jetpack_Media_Meta_Extractor::extract( $blog_id, $post_id, Jetpack_Media_Meta_Extractor::ALL );
  87. if ( empty( $extract['has'] ) ) {
  88. if ( $switched ) {
  89. restore_current_blog();
  90. }
  91. return $return;
  92. }
  93. // Prioritize [some] video embeds.
  94. if ( ! empty( $extract['has']['shortcode'] ) ) {
  95. foreach ( $extract['shortcode'] as $type => $data ) {
  96. switch ( $type ) {
  97. case 'videopress':
  98. case 'wpvideo':
  99. if ( 0 === $return['count']['video'] ) {
  100. // If there is no id on the video, then let's just skip this.
  101. if ( ! isset( $data['id'][0] ) ) {
  102. break;
  103. }
  104. $guid = $data['id'][0];
  105. $video_info = videopress_get_video_details( $guid );
  106. // Only add the video tags if the guid returns a valid videopress object.
  107. if ( $video_info instanceof stdClass ) {
  108. // Continue early if we can't find a Video slug.
  109. if ( empty( $video_info->files->std->mp4 ) ) {
  110. break;
  111. }
  112. $url = sprintf(
  113. 'https://videos.files.wordpress.com/%1$s/%2$s',
  114. $guid,
  115. $video_info->files->std->mp4
  116. );
  117. $thumbnail = $video_info->poster;
  118. if ( ! empty( $thumbnail ) ) {
  119. $return['image'] = $thumbnail;
  120. $return['secure']['image'] = $thumbnail;
  121. }
  122. $return['type'] = 'video';
  123. $return['video'] = esc_url_raw( $url );
  124. $return['video_type'] = 'video/mp4';
  125. $return['secure']['video'] = $return['video'];
  126. }
  127. }
  128. $return['count']['video']++;
  129. break;
  130. case 'youtube':
  131. if ( 0 === $return['count']['video'] ) {
  132. $return['type'] = 'video';
  133. $return['video'] = esc_url_raw( 'http://www.youtube.com/watch?feature=player_embedded&v=' . $extract['shortcode']['youtube']['id'][0] );
  134. $return['image'] = self::get_video_poster( 'youtube', $extract['shortcode']['youtube']['id'][0] );
  135. $return['secure']['video'] = self::https( $return['video'] );
  136. $return['secure']['image'] = self::https( $return['image'] );
  137. }
  138. $return['count']['video']++;
  139. break;
  140. case 'vimeo':
  141. if ( 0 === $return['count']['video'] ) {
  142. $return['type'] = 'video';
  143. $return['video'] = esc_url_raw( 'http://vimeo.com/' . $extract['shortcode']['vimeo']['id'][0] );
  144. $return['secure']['video'] = self::https( $return['video'] );
  145. $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
  146. if ( ! empty( $poster_image ) ) {
  147. $return['image'] = $poster_image;
  148. $poster_url_parts = wp_parse_url( $poster_image );
  149. $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
  150. }
  151. }
  152. $return['count']['video']++;
  153. break;
  154. }
  155. }
  156. }
  157. if ( ! empty( $extract['has']['embed'] ) ) {
  158. foreach ( $extract['embed']['url'] as $embed ) {
  159. if ( preg_match( '/((youtube|vimeo|dailymotion)\.com|youtu.be)/', $embed ) ) {
  160. if ( 0 === $return['count']['video'] ) {
  161. $return['type'] = 'video';
  162. $return['video'] = 'http://' . $embed;
  163. $return['secure']['video'] = self::https( $return['video'] );
  164. if ( false !== strpos( $embed, 'youtube' ) ) {
  165. $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
  166. $return['secure']['image'] = self::https( $return['image'] );
  167. } elseif ( false !== strpos( $embed, 'youtu.be' ) ) {
  168. $youtube_id = jetpack_get_youtube_id( $return['video'] );
  169. $return['video'] = 'http://youtube.com/watch?v=' . $youtube_id . '&feature=youtu.be';
  170. $return['secure']['video'] = self::https( $return['video'] );
  171. $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
  172. $return['secure']['image'] = self::https( $return['image'] );
  173. } elseif ( false !== strpos( $embed, 'vimeo' ) ) {
  174. $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
  175. if ( ! empty( $poster_image ) ) {
  176. $return['image'] = $poster_image;
  177. $poster_url_parts = wp_parse_url( $poster_image );
  178. $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
  179. }
  180. } elseif ( false !== strpos( $embed, 'dailymotion' ) ) {
  181. $return['image'] = str_replace( 'dailymotion.com/video/', 'dailymotion.com/thumbnail/video/', $embed );
  182. $return['image'] = wp_parse_url( $return['image'], PHP_URL_SCHEME ) === null ? 'http://' . $return['image'] : $return['image'];
  183. $return['secure']['image'] = self::https( $return['image'] );
  184. }
  185. }
  186. $return['count']['video']++;
  187. }
  188. }
  189. }
  190. // Do we really want to make the video the primary focus of the post?
  191. if ( 'video' === $return['type'] ) {
  192. $content = wpautop( wp_strip_all_tags( $post->post_content ) );
  193. $paragraphs = explode( '</p>', $content );
  194. $number_of_paragraphs = 0;
  195. foreach ( $paragraphs as $i => $paragraph ) {
  196. // Don't include blank lines as a paragraph.
  197. if ( '' === trim( $paragraph ) ) {
  198. unset( $paragraphs[ $i ] );
  199. continue;
  200. }
  201. $number_of_paragraphs++;
  202. }
  203. $number_of_paragraphs = $number_of_paragraphs - $return['count']['video']; // subtract amount for videos.
  204. // More than 2 paragraph? The video is not the primary focus so we can do some more analysis.
  205. if ( $number_of_paragraphs > 2 ) {
  206. $return['type'] = 'standard';
  207. }
  208. }
  209. // If we don't have any prioritized embed...
  210. if ( 'standard' === $return['type'] ) {
  211. if ( ( ! empty( $extract['has']['gallery'] ) || ! empty( $extract['shortcode']['gallery']['count'] ) ) && ! empty( $extract['image'] ) ) {
  212. // ... Then we prioritize galleries first (multiple images returned)
  213. $return['type'] = 'gallery';
  214. $return['images'] = $extract['image'];
  215. foreach ( $return['images'] as $image ) {
  216. $return['secure']['images'][] = array( 'url' => self::ssl_img( $image['url'] ) );
  217. $return['count']['image']++;
  218. }
  219. } elseif ( ! empty( $extract['has']['image'] ) ) {
  220. // ... Or we try and select a single image that would make sense.
  221. $content = wpautop( wp_strip_all_tags( $post->post_content ) );
  222. $paragraphs = explode( '</p>', $content );
  223. $number_of_paragraphs = 0;
  224. foreach ( $paragraphs as $i => $paragraph ) {
  225. // Don't include 'actual' captions as a paragraph.
  226. if ( false !== strpos( $paragraph, '[caption' ) ) {
  227. unset( $paragraphs[ $i ] );
  228. continue;
  229. }
  230. // Don't include blank lines as a paragraph.
  231. if ( '' === trim( $paragraph ) ) {
  232. unset( $paragraphs[ $i ] );
  233. continue;
  234. }
  235. $number_of_paragraphs++;
  236. }
  237. $return['image'] = $extract['image'][0]['url'];
  238. $return['secure']['image'] = self::ssl_img( $return['image'] );
  239. $return['count']['image']++;
  240. if ( $number_of_paragraphs <= 2 && 1 === count( $extract['image'] ) ) {
  241. // If we have lots of text or images, let's not treat it as an image post, but return its first image.
  242. $return['type'] = 'image';
  243. }
  244. }
  245. }
  246. if ( $switched ) {
  247. restore_current_blog();
  248. }
  249. /**
  250. * Allow a theme or plugin to inspect and ultimately change the media summary.
  251. *
  252. * @since 4.4.0
  253. *
  254. * @param array $data The calculated media summary data.
  255. * @param int $post_id The id of the post this data applies to.
  256. */
  257. $return = apply_filters( 'jetpack_media_summary_output', $return, $post_id );
  258. self::$cache[ $cache_key ] = $return;
  259. return $return;
  260. }
  261. /**
  262. * Converts http to https://
  263. *
  264. * @param string $str URL.
  265. *
  266. * @return string URL.
  267. */
  268. public static function https( $str ) {
  269. return str_replace( 'http://', 'https://', $str );
  270. }
  271. /**
  272. * Returns a Photonized version of the URL.
  273. *
  274. * @param string $url URL.
  275. *
  276. * @return string URL.
  277. */
  278. public static function ssl_img( $url ) {
  279. if ( false !== strpos( $url, 'files.wordpress.com' ) ) {
  280. return self::https( $url );
  281. } else {
  282. return self::https( jetpack_photon_url( $url ) );
  283. }
  284. }
  285. /**
  286. * Get the video poster.
  287. *
  288. * @param string $type Video service.
  289. * @param string $id Video ID for the service.
  290. *
  291. * @return string URL of image thumbnail for the video.
  292. */
  293. public static function get_video_poster( $type, $id ) {
  294. if ( 'videopress' === $type ) {
  295. if ( function_exists( 'video_get_highest_resolution_image_url' ) ) {
  296. return video_get_highest_resolution_image_url( $id );
  297. } elseif ( class_exists( 'VideoPress_Video' ) ) {
  298. $video = new VideoPress_Video( $id );
  299. return $video->poster_frame_uri;
  300. }
  301. } elseif ( 'youtube' === $type ) {
  302. return 'http://img.youtube.com/vi/' . $id . '/0.jpg';
  303. }
  304. }
  305. /**
  306. * Clean text of shortcodes and tags.
  307. *
  308. * @param string $text Dirty text.
  309. *
  310. * @return string Clean text.
  311. */
  312. public static function clean_text( $text ) {
  313. return trim(
  314. preg_replace(
  315. '/[\s]+/',
  316. ' ',
  317. preg_replace(
  318. '@https?://[\S]+@',
  319. '',
  320. strip_shortcodes(
  321. wp_strip_all_tags(
  322. $text
  323. )
  324. )
  325. )
  326. )
  327. );
  328. }
  329. /**
  330. * Retrieve an excerpt for the post summary.
  331. *
  332. * This function works around a suspected problem with Core. If resolved, this function should be simplified.
  333. *
  334. * @link https://github.com/Automattic/jetpack/pull/8510
  335. * @link https://core.trac.wordpress.org/ticket/42814
  336. *
  337. * @param string $post_content The post's content.
  338. * @param string $post_excerpt The post's excerpt. Empty if none was explicitly set.
  339. * @param int $max_words Maximum number of words for the excerpt. Used on wp.com. Default 16.
  340. * @param int $max_chars Maximum characters in the excerpt. Used on wp.com. Default 256.
  341. * @param WP_Post $requested_post The post object.
  342. * @return string Post excerpt.
  343. **/
  344. public static function get_excerpt( $post_content, $post_excerpt, $max_words = 16, $max_chars = 256, $requested_post = null ) {
  345. global $post;
  346. $original_post = $post; // Saving the global for later use.
  347. if ( empty( $post_excerpt ) && function_exists( 'wpcom_enhanced_excerpt_extract_excerpt' ) ) {
  348. return self::clean_text(
  349. wpcom_enhanced_excerpt_extract_excerpt(
  350. array(
  351. 'text' => $post_content,
  352. 'excerpt_only' => true,
  353. 'show_read_more' => false,
  354. 'max_words' => $max_words,
  355. 'max_chars' => $max_chars,
  356. 'read_more_threshold' => 25,
  357. )
  358. )
  359. );
  360. } elseif ( $requested_post instanceof WP_Post ) {
  361. // @todo Refactor to not need to override the global.
  362. // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
  363. $post = $requested_post; // setup_postdata does not set the global.
  364. setup_postdata( $post );
  365. /** This filter is documented in core/src/wp-includes/post-template.php */
  366. $post_excerpt = apply_filters( 'get_the_excerpt', $post_excerpt, $post );
  367. // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
  368. $post = $original_post; // wp_reset_postdata uses the $post global.
  369. wp_reset_postdata();
  370. return self::clean_text( $post_excerpt );
  371. }
  372. return '';
  373. }
  374. /**
  375. * Split a string into an array of words.
  376. *
  377. * @param string $text Post content or excerpt.
  378. *
  379. * @return array Array of words.
  380. */
  381. public static function split_content_in_words( $text ) {
  382. $words = preg_split( '/[\s!?;,.]+/', $text, null, PREG_SPLIT_NO_EMPTY );
  383. // Return an empty array if the split above fails.
  384. return $words ? $words : array();
  385. }
  386. /**
  387. * Get the word count.
  388. *
  389. * @param string $post_content Post content.
  390. *
  391. * @return int Word count.
  392. */
  393. public static function get_word_count( $post_content ) {
  394. return (int) count( self::split_content_in_words( self::clean_text( $post_content ) ) );
  395. }
  396. /**
  397. * Get remainder word count (after the excerpt).
  398. *
  399. * @param string $post_content Post content.
  400. * @param string $excerpt_content Excerpt content.
  401. *
  402. * @return int Number of words after the excerpt.
  403. */
  404. public static function get_word_remaining_count( $post_content, $excerpt_content ) {
  405. $content_word_count = count( self::split_content_in_words( self::clean_text( $post_content ) ) );
  406. $excerpt_word_count = count( self::split_content_in_words( self::clean_text( $excerpt_content ) ) );
  407. return (int) $content_word_count - $excerpt_word_count;
  408. }
  409. /**
  410. * Counts the number of links in a post.
  411. *
  412. * @param string $post_content Post content.
  413. *
  414. * @return false|int Number of links.
  415. */
  416. public static function get_link_count( $post_content ) {
  417. return preg_match_all( '/\<a[\> ]/', $post_content, $matches );
  418. }
  419. }