397 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			397 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * Class Jetpack
 | 
						|
 *
 | 
						|
 * @link      https://github.com/googleforcreators/web-stories-wp
 | 
						|
 *
 | 
						|
 * @copyright 2020 Google LLC
 | 
						|
 * @license   https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Copyright 2020 Google LLC
 | 
						|
 *
 | 
						|
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
 * you may not use this file except in compliance with the License.
 | 
						|
 * You may obtain a copy of the License at
 | 
						|
 *
 | 
						|
 *     https://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 *
 | 
						|
 * Unless required by applicable law or agreed to in writing, software
 | 
						|
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
 * See the License for the specific language governing permissions and
 | 
						|
 * limitations under the License.
 | 
						|
 */
 | 
						|
 | 
						|
declare(strict_types = 1);
 | 
						|
 | 
						|
namespace Google\Web_Stories\Integrations;
 | 
						|
 | 
						|
use Google\Web_Stories\Context;
 | 
						|
use Google\Web_Stories\Media\Media_Source_Taxonomy;
 | 
						|
use Google\Web_Stories\Service_Base;
 | 
						|
use Google\Web_Stories\Story_Post_Type;
 | 
						|
use WP_Post;
 | 
						|
use WP_REST_Response;
 | 
						|
 | 
						|
/**
 | 
						|
 * Class Jetpack.
 | 
						|
 *
 | 
						|
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 | 
						|
 *
 | 
						|
 * @phpstan-type AttachmentData array{
 | 
						|
 *   media_details?: array{
 | 
						|
 *     length?: int,
 | 
						|
 *     length_formatted?: string
 | 
						|
 *   },
 | 
						|
 *   url?: string,
 | 
						|
 *   featured_media_src?: string
 | 
						|
 * }
 | 
						|
 *
 | 
						|
 * @phpstan-type EnhancedAttachmentMetadata array{
 | 
						|
 *   width: int,
 | 
						|
 *   height: int,
 | 
						|
 *   file: string,
 | 
						|
 *   sizes: mixed,
 | 
						|
 *   image_meta: mixed,
 | 
						|
 *   videopress?: array{
 | 
						|
 *     duration: int,
 | 
						|
 *     poster: string,
 | 
						|
 *     width: int,
 | 
						|
 *     height: int,
 | 
						|
 *     file_url_base?: array{
 | 
						|
 *       https: string
 | 
						|
 *     },
 | 
						|
 *     files?: array{
 | 
						|
 *       hd?: array{
 | 
						|
 *         mp4?: string
 | 
						|
 *       }
 | 
						|
 *     }
 | 
						|
 *   }
 | 
						|
 * }
 | 
						|
 */
 | 
						|
class Jetpack extends Service_Base {
 | 
						|
 | 
						|
	/**
 | 
						|
	 * VideoPress Mime type.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 */
 | 
						|
	public const VIDEOPRESS_MIME_TYPE = 'video/videopress';
 | 
						|
 | 
						|
	/**
 | 
						|
	 * VideoPress poster meta key.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 */
 | 
						|
	public const VIDEOPRESS_POSTER_META_KEY = 'videopress_poster_image';
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Media_Source_Taxonomy instance.
 | 
						|
	 *
 | 
						|
	 * @var Media_Source_Taxonomy Experiments instance.
 | 
						|
	 */
 | 
						|
	protected Media_Source_Taxonomy $media_source_taxonomy;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Context instance.
 | 
						|
	 *
 | 
						|
	 * @var Context Context instance.
 | 
						|
	 */
 | 
						|
	private Context $context;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Jetpack constructor.
 | 
						|
	 *
 | 
						|
	 * @since 1.12.0
 | 
						|
	 *
 | 
						|
	 * @param Media_Source_Taxonomy $media_source_taxonomy Media_Source_Taxonomy instance.
 | 
						|
	 * @param Context               $context               Context instance.
 | 
						|
	 */
 | 
						|
	public function __construct( Media_Source_Taxonomy $media_source_taxonomy, Context $context ) {
 | 
						|
		$this->media_source_taxonomy = $media_source_taxonomy;
 | 
						|
		$this->context               = $context;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Initializes all hooks.
 | 
						|
	 *
 | 
						|
	 * @since 1.2.0
 | 
						|
	 */
 | 
						|
	public function register(): void {
 | 
						|
		// See https://github.com/Automattic/jetpack/blob/4b85be883b3c584c64eeb2fb0f3fcc15dabe2d30/modules/custom-post-types/portfolios.php#L80.
 | 
						|
		if ( \defined( 'IS_WPCOM' ) && IS_WPCOM ) {
 | 
						|
			add_filter( 'wpcom_sitemap_post_types', [ $this, 'add_to_jetpack_sitemap' ] );
 | 
						|
		} else {
 | 
						|
			add_filter( 'jetpack_sitemap_post_types', [ $this, 'add_to_jetpack_sitemap' ] );
 | 
						|
		}
 | 
						|
 | 
						|
		add_filter( 'jetpack_is_amp_request', [ $this, 'force_amp_request' ] );
 | 
						|
		add_filter( 'web_stories_allowed_mime_types', [ $this, 'add_videopress' ] );
 | 
						|
		add_filter( 'web_stories_rest_prepare_attachment', [ $this, 'filter_rest_api_response' ], 10, 2 );
 | 
						|
		add_filter( 'ajax_query_attachments_args', [ $this, 'filter_ajax_query_attachments_args' ] );
 | 
						|
		add_action( 'added_post_meta', [ $this, 'add_term' ], 10, 3 );
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Adds the web-story post type to Jetpack / WordPress.com sitemaps.
 | 
						|
	 *
 | 
						|
	 * @since 1.2.0
 | 
						|
	 *
 | 
						|
	 * @see https://github.com/Automattic/jetpack/blob/4b85be883b3c584c64eeb2fb0f3fcc15dabe2d30/modules/custom-post-types/portfolios.php#L80
 | 
						|
	 *
 | 
						|
	 * @param array|mixed $post_types Array of post types.
 | 
						|
	 * @return array|mixed Modified list of post types.
 | 
						|
	 *
 | 
						|
	 * @template T
 | 
						|
	 *
 | 
						|
	 * @phpstan-return ($post_types is array<T> ? array<T> : mixed)
 | 
						|
	 */
 | 
						|
	public function add_to_jetpack_sitemap( $post_types ) {
 | 
						|
		if ( ! \is_array( $post_types ) ) {
 | 
						|
			return $post_types;
 | 
						|
		}
 | 
						|
		$post_types[] = Story_Post_Type::POST_TYPE_SLUG;
 | 
						|
 | 
						|
		return $post_types;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Add VideoPress to allowed mime types.
 | 
						|
	 *
 | 
						|
	 * If the site does not support VideoPress, this will be filtered out.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param array{video?: string[]}|mixed $mime_types Associative array of allowed mime types per media type (image, audio, video).
 | 
						|
	 * @return array{video?: string[]}|mixed
 | 
						|
	 *
 | 
						|
	 * @template T
 | 
						|
	 *
 | 
						|
	 * @phpstan-return ($mime_types is array<T> ? array<T> : mixed)
 | 
						|
	 */
 | 
						|
	public function add_videopress( $mime_types ) {
 | 
						|
		if ( ! \is_array( $mime_types ) ) {
 | 
						|
			return $mime_types;
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Mime types config.
 | 
						|
		 *
 | 
						|
		 * @var array{video?: string[]} $mime_types
 | 
						|
		 */
 | 
						|
		$mime_types['video'][] = self::VIDEOPRESS_MIME_TYPE;
 | 
						|
 | 
						|
		return $mime_types;
 | 
						|
	}
 | 
						|
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Filter ajax query attachments args when accessed from the Web Stories editor.
 | 
						|
	 *
 | 
						|
	 * Only filters the response if the mime type matches exactly what Web Stories is looking for.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param array|mixed $args Query args.
 | 
						|
	 * @return array|mixed Filtered query args.
 | 
						|
	 *
 | 
						|
	 * @template T
 | 
						|
	 *
 | 
						|
	 * @phpstan-return ($args is array<T> ? array<T> : mixed)
 | 
						|
	 */
 | 
						|
	public function filter_ajax_query_attachments_args( $args ) {
 | 
						|
		if ( ! \is_array( $args ) || ! isset( $args['post_mime_type'] ) || ! \is_array( $args['post_mime_type'] ) ) {
 | 
						|
			return $args;
 | 
						|
		}
 | 
						|
 | 
						|
		if ( \in_array( self::VIDEOPRESS_MIME_TYPE, $args['post_mime_type'], true ) ) {
 | 
						|
			add_filter( 'wp_prepare_attachment_for_js', [ $this, 'filter_admin_ajax_response' ], 15, 2 );
 | 
						|
		}
 | 
						|
 | 
						|
		return $args;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Filter admin ajax responses for VideoPress videos.
 | 
						|
	 *
 | 
						|
	 * Changes the video/videopress type back to mp4
 | 
						|
	 * and ensures MP4 source URLs are returned.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param array|mixed $data   Array of prepared attachment data. @see wp_prepare_attachment_for_js().
 | 
						|
	 * @param WP_Post     $attachment Attachment object.
 | 
						|
	 * @return array|mixed
 | 
						|
	 *
 | 
						|
	 * @phpstan-param AttachmentData $data
 | 
						|
	 * @phpstan-return AttachmentData|mixed
 | 
						|
	 *
 | 
						|
	 * @template T
 | 
						|
	 *
 | 
						|
	 * @phpstan-return ($data is array<T> ? array<T> : mixed)
 | 
						|
	 */
 | 
						|
	public function filter_admin_ajax_response( $data, WP_Post $attachment ) {
 | 
						|
		if ( self::VIDEOPRESS_MIME_TYPE !== $attachment->post_mime_type ) {
 | 
						|
			return $data;
 | 
						|
		}
 | 
						|
 | 
						|
		if ( ! \is_array( $data ) ) {
 | 
						|
			return $data;
 | 
						|
		}
 | 
						|
 | 
						|
		// Reset mime type back to mp4, as this is the correct value.
 | 
						|
		$data['mime']    = 'video/mp4';
 | 
						|
		$data['subtype'] = 'mp4';
 | 
						|
 | 
						|
		// Mark video as optimized.
 | 
						|
		$data[ $this->media_source_taxonomy::MEDIA_SOURCE_KEY ] = 'video-optimization';
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Jetpack adds an additional field to regular attachment metadata.
 | 
						|
		 *
 | 
						|
		 * @var array $metadata
 | 
						|
		 * @phpstan-var EnhancedAttachmentMetadata|false $metadata
 | 
						|
		 */
 | 
						|
		$metadata = wp_get_attachment_metadata( $attachment->ID );
 | 
						|
 | 
						|
		if ( $metadata && isset( $metadata['videopress']['duration'], $data['media_details'] ) && \is_array( $data['media_details'] ) ) {
 | 
						|
			$data['media_details']['length_formatted'] = $this->format_milliseconds( $metadata['videopress']['duration'] );
 | 
						|
			$data['media_details']['length']           = (int) floor( $metadata['videopress']['duration'] / 1000 );
 | 
						|
		}
 | 
						|
 | 
						|
		if ( $metadata && isset( $data['url'], $metadata['videopress']['file_url_base']['https'], $metadata['videopress']['files']['hd']['mp4'] ) ) {
 | 
						|
			$data['url'] = $metadata['videopress']['file_url_base']['https'] . $metadata['videopress']['files']['hd']['mp4'];
 | 
						|
		}
 | 
						|
 | 
						|
		// Get the correct poster with matching dimensions from VideoPress.
 | 
						|
		if ( $metadata && isset( $data['featured_media_src'], $metadata['videopress']['poster'], $metadata['videopress']['width'], $metadata['videopress']['height'] ) ) {
 | 
						|
			$data['featured_media_src'] = [
 | 
						|
				'src'       => $metadata['videopress']['poster'],
 | 
						|
				'width'     => $metadata['videopress']['width'],
 | 
						|
				'height'    => $metadata['videopress']['height'],
 | 
						|
				'generated' => true,
 | 
						|
			];
 | 
						|
		}
 | 
						|
 | 
						|
		return $data;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Filter REST API responses for VideoPress videos.
 | 
						|
	 *
 | 
						|
	 * Changes the video/videopress type back to mp4
 | 
						|
	 * and ensures MP4 source URLs are returned.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param WP_REST_Response $response The response object.
 | 
						|
	 * @param WP_Post          $post     The original attachment post.
 | 
						|
	 */
 | 
						|
	public function filter_rest_api_response( WP_REST_Response $response, WP_Post $post ): WP_REST_Response {
 | 
						|
		if ( self::VIDEOPRESS_MIME_TYPE !== $post->post_mime_type ) {
 | 
						|
			return $response;
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Response data.
 | 
						|
		 *
 | 
						|
		 * @var array<string, string|array<string, int|string>|bool> $data
 | 
						|
		 */
 | 
						|
		$data = $response->get_data();
 | 
						|
 | 
						|
		// Reset mime type back to mp4, as this is the correct value.
 | 
						|
		$data['mime_type'] = 'video/mp4';
 | 
						|
 | 
						|
		// Mark video as optimized.
 | 
						|
		$data[ $this->media_source_taxonomy::MEDIA_SOURCE_KEY ] = 'video-optimization';
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Jetpack adds an additional field to regular attachment metadata.
 | 
						|
		 *
 | 
						|
		 * @var EnhancedAttachmentMetadata|false $metadata
 | 
						|
		 */
 | 
						|
		$metadata = wp_get_attachment_metadata( $post->ID );
 | 
						|
 | 
						|
		if ( $metadata && isset( $metadata['videopress']['duration'], $data['media_details'] ) && \is_array( $data['media_details'] ) ) {
 | 
						|
			$data['media_details']['length_formatted'] = $this->format_milliseconds( $metadata['videopress']['duration'] );
 | 
						|
			$data['media_details']['length']           = (int) floor( $metadata['videopress']['duration'] / 1000 );
 | 
						|
		}
 | 
						|
 | 
						|
		if ( $metadata && isset( $data['source_url'], $metadata['videopress']['file_url_base']['https'], $metadata['videopress']['files']['hd']['mp4'] ) ) {
 | 
						|
			$data['source_url'] = $metadata['videopress']['file_url_base']['https'] . $metadata['videopress']['files']['hd']['mp4'];
 | 
						|
		}
 | 
						|
 | 
						|
		// Get the correct poster with matching dimensions from VideoPress.
 | 
						|
		if ( $metadata && isset( $data['featured_media_src'], $metadata['videopress']['poster'], $metadata['videopress']['width'], $metadata['videopress']['height'] ) ) {
 | 
						|
			$data['featured_media_src'] = [
 | 
						|
				'src'       => $metadata['videopress']['poster'],
 | 
						|
				'width'     => $metadata['videopress']['width'],
 | 
						|
				'height'    => $metadata['videopress']['height'],
 | 
						|
				'generated' => true,
 | 
						|
			];
 | 
						|
		}
 | 
						|
 | 
						|
		$response->set_data( $data );
 | 
						|
 | 
						|
		return $response;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Hook into added_post_meta.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param int    $mid         The meta ID after successful update.
 | 
						|
	 * @param int    $object_id   ID of the object metadata is for.
 | 
						|
	 * @param string $meta_key    Metadata key.
 | 
						|
	 */
 | 
						|
	public function add_term( int $mid, int $object_id, string $meta_key ): void {
 | 
						|
		if ( self::VIDEOPRESS_POSTER_META_KEY !== $meta_key ) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		if ( 'attachment' !== get_post_type( $object_id ) ) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		wp_set_object_terms( (int) $object_id, $this->media_source_taxonomy::TERM_POSTER_GENERATION, $this->media_source_taxonomy->get_taxonomy_slug() );
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Force Jetpack to see Web Stories as AMP.
 | 
						|
	 *
 | 
						|
	 * @since 1.2.0
 | 
						|
	 *
 | 
						|
	 * @param bool $is_amp_request Is the request supposed to return valid AMP content.
 | 
						|
	 * @return bool Whether the current request is an AMP request.
 | 
						|
	 */
 | 
						|
	public function force_amp_request( bool $is_amp_request ): bool {
 | 
						|
		if ( ! $this->context->is_web_story() ) {
 | 
						|
			return (bool) $is_amp_request;
 | 
						|
		}
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Format milliseconds into seconds.
 | 
						|
	 *
 | 
						|
	 * @since 1.7.2
 | 
						|
	 *
 | 
						|
	 * @param int $milliseconds Milliseconds to converted to minutes and seconds.
 | 
						|
	 */
 | 
						|
	protected function format_milliseconds( int $milliseconds ): string {
 | 
						|
		$seconds = floor( $milliseconds / 1000 );
 | 
						|
 | 
						|
		if ( $seconds >= 1 ) {
 | 
						|
			$minutes  = floor( $seconds / 60 );
 | 
						|
			$seconds %= 60;
 | 
						|
		} else {
 | 
						|
			$seconds = 0;
 | 
						|
			$minutes = 0;
 | 
						|
		}
 | 
						|
 | 
						|
		return sprintf( '%d:%02u', $minutes, $seconds );
 | 
						|
	}
 | 
						|
}
 |