utility-functions.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <?php
  2. /**
  3. * We won't have any videos less than sixty pixels wide. That would be silly.
  4. */
  5. defined( 'VIDEOPRESS_MIN_WIDTH' ) or define( 'VIDEOPRESS_MIN_WIDTH', 60 );
  6. /**
  7. * Validate user-supplied guid values against expected inputs
  8. *
  9. * @since 1.1
  10. * @param string $guid video identifier
  11. * @return bool true if passes validation test
  12. */
  13. function videopress_is_valid_guid( $guid ) {
  14. if ( ! empty( $guid ) && is_string( $guid ) && strlen( $guid ) === 8 && ctype_alnum( $guid ) ) {
  15. return true;
  16. }
  17. return false;
  18. }
  19. /**
  20. * Get details about a specific video by GUID:
  21. *
  22. * @param $guid string
  23. * @return object
  24. */
  25. function videopress_get_video_details( $guid ) {
  26. if ( ! videopress_is_valid_guid( $guid ) ) {
  27. return new WP_Error( 'bad-guid-format', __( 'Invalid Video GUID!', 'jetpack' ) );
  28. }
  29. $version = '1.1';
  30. $endpoint = sprintf( '/videos/%1$s', $guid );
  31. $query_url = sprintf(
  32. 'https://public-api.wordpress.com/rest/v%1$s%2$s',
  33. $version,
  34. $endpoint
  35. );
  36. // Look for data in our transient. If nothing, let's make a new query.
  37. $data_from_cache = get_transient( 'jetpack_videopress_' . $guid );
  38. if ( false === $data_from_cache ) {
  39. $response = wp_remote_get( esc_url_raw( $query_url ) );
  40. $data = json_decode( wp_remote_retrieve_body( $response ) );
  41. // Cache the response for an hour.
  42. set_transient( 'jetpack_videopress_' . $guid, $data, HOUR_IN_SECONDS );
  43. } else {
  44. $data = $data_from_cache;
  45. }
  46. /**
  47. * Allow functions to modify fetched video details.
  48. *
  49. * This filter allows third-party code to modify the return data
  50. * about a given video. It may involve swapping some data out or
  51. * adding new parameters.
  52. *
  53. * @since 4.0.0
  54. *
  55. * @param object $data The data returned by the WPCOM API. See: https://developer.wordpress.com/docs/api/1.1/get/videos/%24guid/
  56. * @param string $guid The GUID of the VideoPress video in question.
  57. */
  58. return apply_filters( 'videopress_get_video_details', $data, $guid );
  59. }
  60. /**
  61. * Get an attachment ID given a URL.
  62. *
  63. * Modified from http://wpscholar.com/blog/get-attachment-id-from-wp-image-url/
  64. *
  65. * @todo: Add some caching in here.
  66. *
  67. * @param string $url
  68. *
  69. * @return int|bool Attachment ID on success, false on failure
  70. */
  71. function videopress_get_attachment_id_by_url( $url ) {
  72. $wp_upload_dir = wp_upload_dir();
  73. // Strip out protocols, so it doesn't fail because searching for http: in https: dir.
  74. $dir = set_url_scheme( trailingslashit( $wp_upload_dir['baseurl'] ), 'relative' );
  75. // Is URL in uploads directory?
  76. if ( false !== strpos( $url, $dir ) ) {
  77. $file = basename( $url );
  78. $query_args = array(
  79. 'post_type' => 'attachment',
  80. 'post_status' => 'inherit',
  81. 'fields' => 'ids',
  82. 'meta_query' => array(
  83. array(
  84. 'key' => '_wp_attachment_metadata',
  85. 'compare' => 'LIKE',
  86. 'value' => $file,
  87. ),
  88. )
  89. );
  90. $query = new WP_Query( $query_args );
  91. if ( $query->have_posts() ) {
  92. foreach ( $query->posts as $attachment_id ) {
  93. $meta = wp_get_attachment_metadata( $attachment_id );
  94. $original_file = basename( $meta['file'] );
  95. $cropped_files = wp_list_pluck( $meta['sizes'], 'file' );
  96. if ( $original_file === $file || in_array( $file, $cropped_files ) ) {
  97. return (int) $attachment_id;
  98. }
  99. }
  100. }
  101. }
  102. return false;
  103. }
  104. /**
  105. * Similar to `media_sideload_image` -- but returns an ID.
  106. *
  107. * @param $url
  108. * @param $attachment_id
  109. *
  110. * @return int|mixed|object|WP_Error
  111. */
  112. function videopress_download_poster_image( $url, $attachment_id ) {
  113. // Set variables for storage, fix file filename for query strings.
  114. preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $url, $matches );
  115. if ( ! $matches ) {
  116. return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL', 'jetpack' ) );
  117. }
  118. $file_array = array();
  119. $file_array['name'] = basename( $matches[0] );
  120. $file_array['tmp_name'] = download_url( $url );
  121. // If error storing temporarily, return the error.
  122. if ( is_wp_error( $file_array['tmp_name'] ) ) {
  123. return $file_array['tmp_name'];
  124. }
  125. // Do the validation and storage stuff.
  126. $thumbnail_id = media_handle_sideload( $file_array, $attachment_id, null );
  127. // Flag it as poster image, so we can exclude it from display.
  128. update_post_meta( $thumbnail_id, 'videopress_poster_image', 1 );
  129. return $thumbnail_id;
  130. }
  131. /**
  132. * Creates a local media library item of a remote VideoPress video.
  133. *
  134. * @param $guid
  135. * @param int $parent_id
  136. *
  137. * @return int|object
  138. */
  139. function create_local_media_library_for_videopress_guid( $guid, $parent_id = 0 ) {
  140. $vp_data = videopress_get_video_details( $guid );
  141. if ( ! $vp_data || is_wp_error( $vp_data ) ) {
  142. return $vp_data;
  143. }
  144. $args = array(
  145. 'post_date' => $vp_data->upload_date,
  146. 'post_title' => wp_kses( $vp_data->title, array() ),
  147. 'post_content' => wp_kses( $vp_data->description, array() ),
  148. 'post_mime_type' => 'video/videopress',
  149. 'guid' => sprintf( 'https://videopress.com/v/%s', $guid ),
  150. );
  151. $attachment_id = wp_insert_attachment( $args, null, $parent_id );
  152. if ( ! is_wp_error( $attachment_id ) ) {
  153. update_post_meta( $attachment_id, 'videopress_guid', $guid );
  154. wp_update_attachment_metadata( $attachment_id, array(
  155. 'width' => $vp_data->width,
  156. 'height' => $vp_data->height,
  157. ) );
  158. $thumbnail_id = videopress_download_poster_image( $vp_data->poster, $attachment_id );
  159. update_post_meta( $attachment_id, '_thumbnail_id', $thumbnail_id );
  160. }
  161. return $attachment_id;
  162. }
  163. /**
  164. * Helper that will look for VideoPress media items that are more than 30 minutes old,
  165. * that have not had anything attached to them by a wpcom upload and deletes the ghost
  166. * attachment.
  167. *
  168. * These happen primarily because of failed upload attempts.
  169. *
  170. * @return int The number of items that were cleaned up.
  171. */
  172. function videopress_cleanup_media_library() {
  173. // Disable this job for now.
  174. return 0;
  175. $query_args = array(
  176. 'post_type' => 'attachment',
  177. 'post_status' => 'inherit',
  178. 'post_mime_type' => 'video/videopress',
  179. 'meta_query' => array(
  180. array(
  181. 'key' => 'videopress_status',
  182. 'value' => 'new',
  183. ),
  184. )
  185. );
  186. $query = new WP_Query( $query_args );
  187. $cleaned = 0;
  188. $now = current_time( 'timestamp' );
  189. if ( $query->have_posts() ) {
  190. foreach ( $query->posts as $post ) {
  191. $post_time = strtotime( $post->post_date_gmt );
  192. // If the post is older than 30 minutes, it is safe to delete it.
  193. if ( $now - $post_time > MINUTE_IN_SECONDS * 30 ) {
  194. // Force delete the attachment, because we don't want it appearing in the trash.
  195. wp_delete_attachment( $post->ID, true );
  196. $cleaned++;
  197. }
  198. }
  199. }
  200. return $cleaned;
  201. }
  202. /**
  203. * Return an absolute URI for a given filename and guid on the CDN.
  204. * No check is performed to ensure the guid exists or the file is present. Simple centralized string builder.
  205. *
  206. * @param string $guid VideoPress identifier
  207. * @param string $filename name of file associated with the guid (video file name or thumbnail file name)
  208. *
  209. * @return string Absolute URL of VideoPress file for the given guid.
  210. */
  211. function videopress_cdn_file_url( $guid, $filename ) {
  212. return "https://videos.files.wordpress.com/{$guid}/{$filename}";
  213. }
  214. /**
  215. * Get an array of the transcoding status for the given video post.
  216. *
  217. * @since 4.4
  218. * @param int $post_id
  219. * @return array|bool Returns an array of statuses if this is a VideoPress post, otherwise it returns false.
  220. */
  221. function videopress_get_transcoding_status( $post_id ) {
  222. $meta = wp_get_attachment_metadata( $post_id );
  223. // If this has not been processed by videopress, we can skip the rest.
  224. if ( ! $meta || ! isset( $meta['file_statuses'] ) ) {
  225. return false;
  226. }
  227. $info = (object) $meta['file_statuses'];
  228. $status = array(
  229. 'std_mp4' => isset( $info->mp4 ) ? $info->mp4 : null,
  230. 'std_ogg' => isset( $info->ogg ) ? $info->ogg : null,
  231. 'dvd_mp4' => isset( $info->dvd ) ? $info->dvd : null,
  232. 'hd_mp4' => isset( $info->hd ) ? $info->hd : null,
  233. );
  234. return $status;
  235. }
  236. /**
  237. * Get the direct url to the video.
  238. *
  239. * @since 4.4
  240. * @param string $guid
  241. * @return string
  242. */
  243. function videopress_build_url( $guid ) {
  244. // No guid, no videopress url.
  245. if ( ! $guid ) {
  246. return '';
  247. }
  248. return 'https://videopress.com/v/' . $guid;
  249. }
  250. /**
  251. * Create an empty videopress media item that will be filled out later by an xmlrpc
  252. * callback from the VideoPress servers.
  253. *
  254. * @since 4.4
  255. * @param string $title
  256. * @return int|WP_Error
  257. */
  258. function videopress_create_new_media_item( $title, $guid = null ) {
  259. $post = array(
  260. 'post_type' => 'attachment',
  261. 'post_mime_type' => 'video/videopress',
  262. 'post_title' => $title,
  263. 'post_content' => '',
  264. 'guid' => videopress_build_url( $guid ),
  265. );
  266. $media_id = wp_insert_post( $post );
  267. add_post_meta( $media_id, 'videopress_status', 'initiated' );
  268. add_post_meta( $media_id, 'videopress_guid', $guid );
  269. return $media_id;
  270. }
  271. /**
  272. * @param array $current_status
  273. * @param array $new_meta
  274. * @return array
  275. */
  276. function videopress_merge_file_status( $current_status, $new_meta ) {
  277. $new_statuses = array();
  278. if ( isset( $new_meta['videopress']['files_status']['hd'] ) ) {
  279. $new_statuses['hd'] = $new_meta['videopress']['files_status']['hd'];
  280. }
  281. if ( isset( $new_meta['videopress']['files_status']['dvd'] ) ) {
  282. $new_statuses['dvd'] = $new_meta['videopress']['files_status']['dvd'];
  283. }
  284. if ( isset( $new_meta['videopress']['files_status']['std']['mp4'] ) ) {
  285. $new_statuses['mp4'] = $new_meta['videopress']['files_status']['std']['mp4'];
  286. }
  287. if ( isset( $new_meta['videopress']['files_status']['std']['ogg'] ) ) {
  288. $new_statuses['ogg'] = $new_meta['videopress']['files_status']['std']['ogg'];
  289. }
  290. foreach ( $new_statuses as $format => $status ) {
  291. if ( ! isset( $current_status[ $format ] ) ) {
  292. $current_status[ $format ] = $status;
  293. continue;
  294. }
  295. if ( $current_status[ $format ] !== 'DONE' ) {
  296. $current_status[ $format ] = $status;
  297. }
  298. }
  299. return $current_status;
  300. }
  301. /**
  302. * Check to see if a video has completed processing.
  303. *
  304. * @since 4.4
  305. * @param int $post_id
  306. * @return bool
  307. */
  308. function videopress_is_finished_processing( $post_id ) {
  309. $post = get_post( $post_id );
  310. if ( is_wp_error( $post ) ) {
  311. return false;
  312. }
  313. $meta = wp_get_attachment_metadata( $post->ID );
  314. if ( ! isset( $meta['file_statuses'] ) || ! is_array( $meta['file_statuses'] ) ) {
  315. return false;
  316. }
  317. $check_statuses = array( 'hd', 'dvd', 'mp4', 'ogg' );
  318. foreach ( $check_statuses as $status ) {
  319. if ( ! isset( $meta['file_statuses'][ $status ] ) || $meta['file_statuses'][ $status ] != 'DONE' ) {
  320. return false;
  321. }
  322. }
  323. return true;
  324. }
  325. /**
  326. * Update the meta information status for the given video post.
  327. *
  328. * @since 4.4
  329. * @param int $post_id
  330. * @return bool
  331. */
  332. function videopress_update_meta_data( $post_id ) {
  333. $meta = wp_get_attachment_metadata( $post_id );
  334. // If this has not been processed by VideoPress, we can skip the rest.
  335. if ( ! $meta || ! isset( $meta['videopress'] ) ) {
  336. return false;
  337. }
  338. $info = (object) $meta['videopress'];
  339. $args = array(
  340. // 'sslverify' => false,
  341. );
  342. $result = wp_remote_get( videopress_make_video_get_path( $info->guid ), $args );
  343. if ( is_wp_error( $result ) ) {
  344. return false;
  345. }
  346. $response = json_decode( $result['body'], true );
  347. // Update the attachment metadata.
  348. $meta['videopress'] = $response;
  349. wp_update_attachment_metadata( $post_id, $meta );
  350. return true;
  351. }
  352. /**
  353. * Check to see if this is a VideoPress post that hasn't had a guid set yet.
  354. *
  355. * @param int $post_id
  356. * @return bool
  357. */
  358. function videopress_is_attachment_without_guid( $post_id ) {
  359. $post = get_post( $post_id );
  360. if ( is_wp_error( $post ) ) {
  361. return false;
  362. }
  363. if ( $post->post_mime_type !== 'video/videopress' ) {
  364. return false;
  365. }
  366. $videopress_guid = get_post_meta( $post_id, 'videopress_guid', true );
  367. if ( $videopress_guid ) {
  368. return false;
  369. }
  370. return true;
  371. }
  372. /**
  373. * Check to see if this is a VideoPress attachment.
  374. *
  375. * @param int $post_id
  376. * @return bool
  377. */
  378. function is_videopress_attachment( $post_id ) {
  379. $post = get_post( $post_id );
  380. if ( is_wp_error( $post ) ) {
  381. return false;
  382. }
  383. if ( $post->post_mime_type !== 'video/videopress' ) {
  384. return false;
  385. }
  386. return true;
  387. }
  388. /**
  389. * Get the video update path
  390. *
  391. * @since 4.4
  392. * @param string $guid
  393. * @return string
  394. */
  395. function videopress_make_video_get_path( $guid ) {
  396. return sprintf(
  397. '%s://%s/rest/v%s/videos/%s',
  398. 'https',
  399. JETPACK__WPCOM_JSON_API_HOST,
  400. Jetpack_Client::WPCOM_JSON_API_VERSION,
  401. $guid
  402. );
  403. }
  404. /**
  405. * Get the upload api path.
  406. *
  407. * @since 4.4
  408. * @param int $blog_id The id of the blog we're uploading to.
  409. * @return string
  410. */
  411. function videopress_make_media_upload_path( $blog_id ) {
  412. return sprintf(
  413. 'https://public-api.wordpress.com/rest/v1.1/sites/%s/media/new',
  414. $blog_id
  415. );
  416. }
  417. /**
  418. * This is a mock of the internal VideoPress method, which is meant to duplicate the functionality
  419. * of the WPCOM API, so that the Jetpack REST API returns the same data with no modifications.
  420. *
  421. * @param int $blog_id Blog ID.
  422. * @param int $post_id Post ID.
  423. * @return bool|stdClass
  424. */
  425. function video_get_info_by_blogpostid( $blog_id, $post_id ) {
  426. $post = get_post( $post_id );
  427. $video_info = new stdClass();
  428. $video_info->post_id = $post_id;
  429. $video_info->blog_id = $blog_id;
  430. $video_info->guid = null;
  431. $video_info->finish_date_gmt = '0000-00-00 00:00:00';
  432. if ( is_wp_error( $post ) ) {
  433. return $video_info;
  434. }
  435. if ( 'video/videopress' !== $post->post_mime_type ) {
  436. return $video_info;
  437. }
  438. // Since this is a VideoPress post, lt's fill out the rest of the object.
  439. $video_info->guid = get_post_meta( $post_id, 'videopress_guid', true );
  440. if ( videopress_is_finished_processing( $post_id ) ) {
  441. $video_info->finish_date_gmt = date( 'Y-m-d H:i:s' );
  442. }
  443. return $video_info;
  444. }
  445. /**
  446. * Check that a VideoPress video format has finished processing.
  447. *
  448. * This uses the info object, because that is what the WPCOM endpoint
  449. * uses, however we don't have a complete info object in the same way
  450. * WPCOM does, so we pull the meta information out of the post
  451. * options instead.
  452. *
  453. * Note: This mimics the WPCOM function of the same name and helps the media
  454. * API endpoint add all needed VideoPress data.
  455. *
  456. * @param stdClass $info
  457. * @param string $format
  458. * @return bool
  459. */
  460. function video_format_done( $info, $format ) {
  461. // Avoids notice when a non-videopress item is found.
  462. if ( ! is_object( $info ) ) {
  463. return false;
  464. }
  465. $post_id = $info->post_id;
  466. if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
  467. return false;
  468. }
  469. $post = get_post( $post_id );
  470. if ( is_wp_error( $post ) ) {
  471. return false;
  472. }
  473. $meta = wp_get_attachment_metadata( $post->ID );
  474. switch ( $format ) {
  475. case 'fmt_hd':
  476. return isset( $meta['videopress']['files']['hd']['mp4'] );
  477. break;
  478. case 'fmt_dvd':
  479. return isset( $meta['videopress']['files']['dvd']['mp4'] );
  480. break;
  481. case 'fmt_std':
  482. return isset( $meta['videopress']['files']['std']['mp4'] );
  483. break;
  484. case 'fmt_ogg':
  485. return isset( $meta['videopress']['files']['std']['ogg'] );
  486. break;
  487. }
  488. return false;
  489. }
  490. /**
  491. * Get the image URL for the given VideoPress GUID
  492. *
  493. * We look up by GUID, because that is what WPCOM does and this needs to be
  494. * parameter compatible with that.
  495. *
  496. * Note: This mimics the WPCOM function of the same name and helps the media
  497. * API endpoint add all needed VideoPress data.
  498. *
  499. * @param string $guid
  500. * @param string $format
  501. * @return string
  502. */
  503. function video_image_url_by_guid( $guid, $format ) {
  504. $post = video_get_post_by_guid( $guid );
  505. if ( is_wp_error( $post ) ) {
  506. return null;
  507. }
  508. $meta = wp_get_attachment_metadata( $post->ID );
  509. // We add ssl => 1 to make sure that the videos.files.wordpress.com domain is parsed as photon.
  510. $poster = apply_filters( 'jetpack_photon_url', $meta['videopress']['poster'], array( 'ssl' => 1 ), 'https' );
  511. return $poster;
  512. }
  513. /**
  514. * Using a GUID, find a post.
  515. *
  516. * @param string $guid
  517. * @return WP_Post
  518. */
  519. function video_get_post_by_guid( $guid ) {
  520. $args = array(
  521. 'post_type' => 'attachment',
  522. 'post_mime_type' => 'video/videopress',
  523. 'post_status' => 'inherit',
  524. 'meta_query' => array(
  525. array(
  526. 'key' => 'videopress_guid',
  527. 'value' => $guid,
  528. 'compare' => '=',
  529. )
  530. )
  531. );
  532. $query = new WP_Query( $args );
  533. $post = $query->next_post();
  534. return $post;
  535. }
  536. /**
  537. * From the given VideoPress post_id, return back the appropriate attachment URL.
  538. *
  539. * When the MP4 hasn't been processed yet or this is not a VideoPress video, this will return null.
  540. *
  541. * @param int $post_id
  542. * @return string|null
  543. */
  544. function videopress_get_attachment_url( $post_id ) {
  545. // We only handle VideoPress attachments.
  546. if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
  547. return null;
  548. }
  549. $meta = wp_get_attachment_metadata( $post_id );
  550. if ( ! isset( $meta['videopress']['files']['hd']['mp4'] ) ) {
  551. // Use the original file as the url if it isn't transcoded yet.
  552. if ( isset( $meta['original'] ) ) {
  553. return $meta['original'];
  554. }
  555. // Otherwise, there isn't much we can do.
  556. return null;
  557. }
  558. return $meta['videopress']['file_url_base']['https'] . $meta['videopress']['files']['hd']['mp4'];
  559. }