initial
This commit is contained in:
+522
@@ -0,0 +1,522 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Access Control.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
|
||||
use Automattic\Jetpack\Modules;
|
||||
use VIDEOPRESS_PRIVACY;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* VideoPress video access control utilities.
|
||||
*
|
||||
* Note: this is also being used on WordPress.com.
|
||||
* Use IS_WPCOM checks for functionality that is specific to WPCOM/Jetpack.
|
||||
*/
|
||||
class Access_Control {
|
||||
|
||||
/**
|
||||
* Singleton Access_Control instance.
|
||||
*
|
||||
* @var Access_Control
|
||||
**/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Guid to subscription plan, store, for when used inline on a page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $guids_to_subscriptions = array();
|
||||
|
||||
/**
|
||||
* Set that this guid is controlled by a subscription.
|
||||
*
|
||||
* @param string $guid The guid to set.
|
||||
* @param string|int $subscription_id The subscription to set.
|
||||
*
|
||||
* @return Access_Control
|
||||
*/
|
||||
public function set_guid_subscription( $guid, $subscription_id ) {
|
||||
$this->guids_to_subscriptions[ $guid ] = $subscription_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription for a guid.
|
||||
*
|
||||
* @param string $guid The guid to get.
|
||||
*
|
||||
* @return string|int|false
|
||||
*/
|
||||
public function get_subscription_plan_id( $guid ) {
|
||||
return $this->guids_to_subscriptions[ $guid ] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if Jetpack Memberships are available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function jetpack_memberships_available() {
|
||||
return class_exists( '\Jetpack_Memberships' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if Jetpack Subscriptions are available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function jetpack_subscriptions_available() {
|
||||
$is_module_active = ( new Modules() )->is_active( 'subscriptions' );
|
||||
if ( ! $is_module_active ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\subscription_service' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscription_service_file_path = JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
|
||||
if ( ! file_exists( $subscription_service_file_path ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
require_once $subscription_service_file_path;
|
||||
|
||||
return function_exists( '\Automattic\Jetpack\Extensions\Premium_Content\subscription_service' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check default user access. By default, subscribers or higher can view videos.
|
||||
*
|
||||
* @param WP_Post $post_to_check The post to check.
|
||||
*
|
||||
* @return bool
|
||||
**/
|
||||
private function get_default_user_capability_for_post( $post_to_check ) {
|
||||
if ( ! isset( $post_to_check->ID ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$default_auth = current_user_can( 'read_post', $post_to_check->ID );
|
||||
|
||||
return $default_auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user can access restricted content and builds the restriction_details array.
|
||||
*
|
||||
* @param string $guid the video guid.
|
||||
* @param int $embedded_post_id the post id.
|
||||
* @param int $selected_plan_id the selected plan id if applicable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function build_restriction_details( $guid, $embedded_post_id, $selected_plan_id ) {
|
||||
$post_to_check = get_post( $embedded_post_id );
|
||||
|
||||
if ( empty( $post_to_check ) ) {
|
||||
$restriction_details = $this->default_video_restriction_details( false );
|
||||
return $this->filter_video_restriction_details( $restriction_details, $guid, $embedded_post_id, $selected_plan_id );
|
||||
}
|
||||
|
||||
$default_auth = $this->get_default_user_capability_for_post( $post_to_check );
|
||||
$restriction_details = $this->default_video_restriction_details( $default_auth );
|
||||
|
||||
if ( $this->jetpack_memberships_available() ) {
|
||||
$post_access_level = \Jetpack_Memberships::get_post_access_level( $embedded_post_id );
|
||||
if ( 'everybody' !== $post_access_level ) {
|
||||
$memberships_can_view_post = \Jetpack_Memberships::user_can_view_post( $embedded_post_id );
|
||||
$restriction_details = $this->get_subscriber_only_restriction_details( $default_auth );
|
||||
$restriction_details['can_access'] = $memberships_can_view_post;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->check_block_level_access(
|
||||
$restriction_details,
|
||||
$guid,
|
||||
$embedded_post_id,
|
||||
$selected_plan_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user can access restricted block content and updates the restriction_details array.
|
||||
*
|
||||
* @param array $restriction_details the restriction details array.
|
||||
* @param string $guid the video guid.
|
||||
* @param int $embedded_post_id the post id.
|
||||
* @param int $selected_plan_id the selected plan id if applicable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function check_block_level_access( $restriction_details, $guid, $embedded_post_id, $selected_plan_id ) {
|
||||
if ( $this->jetpack_subscriptions_available() && $selected_plan_id > 0 ) {
|
||||
$restriction_details = $this->get_subscriber_only_restriction_details( $restriction_details['can_access'] );
|
||||
$paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
|
||||
|
||||
// Only paid subscribers should be granted access to the premium content.
|
||||
$access_level = '';
|
||||
if ( class_exists( Abstract_Token_Subscription_Service::class ) ) {
|
||||
$access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS;
|
||||
}
|
||||
|
||||
$can_view = $paywall->visitor_can_view_content( array( $selected_plan_id ), $access_level );
|
||||
$restriction_details['can_access'] = $can_view || current_user_can( 'edit_post', $embedded_post_id ); // Editors can always view the content.
|
||||
}
|
||||
|
||||
return $this->filter_video_restriction_details(
|
||||
$restriction_details,
|
||||
$guid,
|
||||
$embedded_post_id,
|
||||
$selected_plan_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default restriction_details for a video.
|
||||
*
|
||||
* @param bool $default_can_access The default auth.
|
||||
*
|
||||
* @return array
|
||||
**/
|
||||
private function get_subscriber_only_restriction_details( $default_can_access = false ) {
|
||||
return array(
|
||||
'provider' => 'jetpack_memberships',
|
||||
'title' => __( 'This video is subscriber-only', 'jetpack-videopress-pkg' ),
|
||||
'unauthorized_message' => __( 'You need to be subscribed to view this video', 'jetpack-videopress-pkg' ),
|
||||
'can_access' => $default_can_access,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters restriction details.
|
||||
*
|
||||
* @param array $video_restriction_details The restriction details.
|
||||
* @param string $guid The video guid.
|
||||
* @param int $embedded_post_id The post id.
|
||||
* @param int $selected_plan_id The selected plan id if applicable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function filter_video_restriction_details( $video_restriction_details, $guid, $embedded_post_id, $selected_plan_id ) {
|
||||
/**
|
||||
* Filters the video restriction details.
|
||||
*
|
||||
* @param array $video_restriction_details The restriction details.
|
||||
* @param string $guid The video guid.
|
||||
* @param int $embedded_post_id The post id.
|
||||
* @param int $selected_plan_id The selected plan id if applicable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
return (array) apply_filters( 'videopress_video_restriction_details', $video_restriction_details, $guid, $embedded_post_id, $selected_plan_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default restriction_details for a video.
|
||||
*
|
||||
* @param bool $default_can_access The default auth.
|
||||
*
|
||||
* @return array
|
||||
**/
|
||||
private function default_video_restriction_details( $default_can_access = false ) {
|
||||
$restriction_details = array(
|
||||
'version' => '1',
|
||||
'provider' => 'auth',
|
||||
'title' => __( 'Unauthorized', 'jetpack-videopress-pkg' ),
|
||||
'unauthorized_message' => __( 'Unauthorized', 'jetpack-videopress-pkg' ),
|
||||
'can_access' => $default_can_access,
|
||||
);
|
||||
|
||||
return $restriction_details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given post actually embeds a given VideoPress GUID.
|
||||
*
|
||||
* Used to prevent the embedded post id — which arrives from request input — from being
|
||||
* treated as an authorization context when it has no relationship to the requested video.
|
||||
* Matching the attachment id itself is not treated as proof of embedding: attachment ids
|
||||
* are enumerable via the media REST route and would otherwise provide a second path around
|
||||
* this check whenever the attachment has no parent and falls back to the `read` capability.
|
||||
*
|
||||
* @param int $embedded_post_id The post id claimed as the embedding context.
|
||||
* @param string $guid The video guid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function post_embeds_videopress_guid( $embedded_post_id, $guid ) {
|
||||
$post = get_post( $embedded_post_id );
|
||||
if ( ! $post instanceof WP_Post || empty( $post->post_content ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the guid is nowhere in the content, neither the block nor the shortcode scan can match.
|
||||
if ( false === strpos( $post->post_content, $guid ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->post_content_has_videopress_block( $post->post_content, $guid ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $this->post_content_has_videopress_shortcode( $post->post_content, $guid ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->post_content_has_videopress_url( $post->post_content, $guid );
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk parsed blocks (including inner blocks) looking for a videopress/video block
|
||||
* whose guid attribute matches.
|
||||
*
|
||||
* @param string $post_content The post content to scan.
|
||||
* @param string $guid The video guid to match.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function post_content_has_videopress_block( $post_content, $guid ) {
|
||||
if ( false === strpos( $post_content, 'wp:videopress/video' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->blocks_contain_videopress_guid( parse_blocks( $post_content ), $guid );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scans a parsed block tree for a videopress/video block whose guid attribute matches.
|
||||
*
|
||||
* @param array $blocks Parsed blocks (as returned by parse_blocks() or an innerBlocks array).
|
||||
* @param string $guid The video guid to match.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function blocks_contain_videopress_guid( $blocks, $guid ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
if (
|
||||
isset( $block['blockName'] ) && 'videopress/video' === $block['blockName']
|
||||
&& isset( $block['attrs']['guid'] ) && $block['attrs']['guid'] === $guid
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] )
|
||||
&& $this->blocks_contain_videopress_guid( $block['innerBlocks'], $guid )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a [videopress GUID] or [wpvideo GUID] shortcode whose first positional
|
||||
* argument matches the given guid.
|
||||
*
|
||||
* @param string $post_content The post content to scan.
|
||||
* @param string $guid The video guid to match.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function post_content_has_videopress_shortcode( $post_content, $guid ) {
|
||||
if ( false === stripos( $post_content, '[videopress' ) && false === stripos( $post_content, '[wpvideo' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = get_shortcode_regex( array( 'videopress', 'wpvideo' ) );
|
||||
$count = preg_match_all( '/' . $pattern . '/', $post_content, $matches, PREG_SET_ORDER );
|
||||
if ( false === $count || 0 === $count ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $matches as $match ) {
|
||||
$atts = shortcode_parse_atts( $match[3] );
|
||||
if ( ! is_array( $atts ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only the positional argument identifies the video; named attributes must not satisfy the binding check.
|
||||
if ( isset( $atts[0] ) && is_string( $atts[0] ) && $atts[0] === $guid ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a canonical VideoPress URL referencing the given guid. Covers oEmbed
|
||||
* inserts, core/embed blocks, core/video blocks, and core [video] shortcodes
|
||||
* whose src/mp4 attributes resolve to a VideoPress URL.
|
||||
*
|
||||
* @param string $post_content The post content to scan.
|
||||
* @param string $guid The video guid to match.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function post_content_has_videopress_url( $post_content, $guid ) {
|
||||
if ( ! preg_match_all( '#https?://[^\s"\'<>)]+#i', $post_content, $matches ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $matches[0] as $url ) {
|
||||
if ( $guid === Utils::extract_videopress_guid_from_url( $url ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user can view the provided video. Only ever gets fired if site-wide private videos are enabled.
|
||||
*
|
||||
* Filterable for 3rd party plugins.
|
||||
*
|
||||
* @param string $guid The video id being checked.
|
||||
* @param int $embedded_post_id The post id the video is embedded in or 0.
|
||||
* @param int $selected_plan_id The plan id the earn block this video is embedded in has.
|
||||
*/
|
||||
public function is_current_user_authed_for_video( $guid, $embedded_post_id, $selected_plan_id = 0 ) {
|
||||
if ( current_user_can( 'upload_files' ) ) {
|
||||
return $this->filter_is_current_user_authed_for_video( true, $guid, $embedded_post_id );
|
||||
}
|
||||
|
||||
$attachment = false;
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$video_info = video_get_info_by_guid( $guid );
|
||||
if ( ! empty( $video_info ) ) {
|
||||
$attachment = get_blog_post( $video_info->blog_id, $video_info->post_id );
|
||||
}
|
||||
} else {
|
||||
$attachment = videopress_get_post_by_guid( $guid );
|
||||
}
|
||||
|
||||
if ( ! $attachment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$video_info = video_get_info_by_blogpostid( get_current_blog_id(), $attachment->ID );
|
||||
if ( null === $video_info->guid ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Default missing privacy_setting to SITE_DEFAULT to avoid an
|
||||
* undefined-property warning and make the site-level fallback explicit.
|
||||
*/
|
||||
$privacy_setting = $video_info->privacy_setting ?? VIDEOPRESS_PRIVACY::SITE_DEFAULT;
|
||||
|
||||
$embedded_post_id = (int) $embedded_post_id;
|
||||
if (
|
||||
$embedded_post_id
|
||||
&& VIDEOPRESS_PRIVACY::IS_PUBLIC !== $privacy_setting
|
||||
&& ! $this->post_embeds_videopress_guid( $embedded_post_id, $guid )
|
||||
) {
|
||||
$embedded_post_id = 0;
|
||||
}
|
||||
|
||||
$is_user_authed = false;
|
||||
|
||||
// Determine if video is public, private or use site default.
|
||||
switch ( $privacy_setting ) {
|
||||
case VIDEOPRESS_PRIVACY::IS_PUBLIC:
|
||||
$is_user_authed = true;
|
||||
break;
|
||||
case VIDEOPRESS_PRIVACY::IS_PRIVATE:
|
||||
$restriction_details = $this->build_restriction_details( $guid, $embedded_post_id, $selected_plan_id );
|
||||
$is_user_authed = $restriction_details['can_access'];
|
||||
break;
|
||||
case VIDEOPRESS_PRIVACY::SITE_DEFAULT:
|
||||
default:
|
||||
$is_videopress_private_for_site = Data::get_videopress_videos_private_for_site();
|
||||
$is_user_authed = true;
|
||||
if ( $is_videopress_private_for_site ) {
|
||||
$restriction_details = $this->build_restriction_details( $guid, $embedded_post_id, $selected_plan_id );
|
||||
$is_user_authed = $restriction_details['can_access'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides video view authorization for current user.
|
||||
*
|
||||
* Example of making all videos public:
|
||||
*
|
||||
* function jp_example_override_video_auth( $is_user_authed, $guid ) {
|
||||
* return true
|
||||
* };
|
||||
* add_filter( 'videopress_is_current_user_authed_for_video', 'jp_example_override_video_auth', 10, 2 );
|
||||
*
|
||||
* @param bool $is_user_authed The current user authorization state.
|
||||
* @param string $guid The video's unique identifier.
|
||||
* @param int|null $embedded_post_id The post the video is embedded..
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
return $this->filter_is_current_user_authed_for_video( $is_user_authed, $guid, $embedded_post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides video view authorization for current user.
|
||||
*
|
||||
* @param bool $is_user_authed The current user authorization state.
|
||||
* @param string $guid The video's unique identifier.
|
||||
* @param int|null $embedded_post_id The post the video is embedded..
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function filter_is_current_user_authed_for_video( $is_user_authed, $guid, $embedded_post_id ) {
|
||||
/**
|
||||
* Overrides video view authorization for current user.
|
||||
*
|
||||
* Example of making all videos public:
|
||||
*
|
||||
* function jp_example_override_video_auth( $is_user_authed, $guid ) {
|
||||
* return true
|
||||
* };
|
||||
* add_filter( 'videopress_is_current_user_authed_for_video', 'jp_example_override_video_auth', 10, 2 );
|
||||
*
|
||||
* @param bool $is_user_authed The current user authorization state.
|
||||
* @param string $guid The video's unique identifier.
|
||||
* @param int|null $embedded_post_id The post the video is embedded..
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
return (bool) apply_filters( 'videopress_is_current_user_authed_for_video', $is_user_authed, $guid, $embedded_post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proper blog id depending on Jetpack or WP.com
|
||||
*
|
||||
* @return int the blog id
|
||||
*/
|
||||
public function get_videopress_blog_id() {
|
||||
return \Jetpack_Options::get_option( 'id' );
|
||||
}
|
||||
}
|
||||
+544
File diff suppressed because one or more lines are too long
+293
@@ -0,0 +1,293 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
|
||||
/**
|
||||
* VideoPress AJAX action handlers and utilities.
|
||||
*
|
||||
* Note: this is also being used on WordPress.com.
|
||||
* Use IS_WPCOM checks for functionality that is specific to WPCOM/Jetpack.
|
||||
*/
|
||||
class AJAX {
|
||||
|
||||
/**
|
||||
* Singleton AJAX instance.
|
||||
*
|
||||
* @var AJAX
|
||||
**/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Private AJAX constructor.
|
||||
*
|
||||
* Use the AJAX::init() method to get an instance.
|
||||
*/
|
||||
private function __construct() {
|
||||
add_action( 'wp_ajax_videopress-get-upload-token', array( $this, 'wp_ajax_videopress_get_upload_token' ) );
|
||||
add_action( 'wp_ajax_videopress-get-upload-jwt', array( $this, 'wp_ajax_videopress_get_upload_jwt' ) );
|
||||
add_action( 'wp_ajax_nopriv_videopress-get-playback-jwt', array( $this, 'wp_ajax_videopress_get_playback_jwt' ) );
|
||||
add_action( 'wp_ajax_videopress-get-playback-jwt', array( $this, 'wp_ajax_videopress_get_playback_jwt' ) );
|
||||
|
||||
add_action(
|
||||
'wp_ajax_videopress-update-transcoding-status',
|
||||
array(
|
||||
$this,
|
||||
'wp_ajax_update_transcoding_status',
|
||||
),
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AJAX and get back a singleton instance.
|
||||
*
|
||||
* @return AJAX
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new AJAX();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a guid.
|
||||
*
|
||||
* @param string $guid The guid to validate.
|
||||
*
|
||||
* @return bool
|
||||
**/
|
||||
private function is_valid_guid( $guid ) {
|
||||
if ( empty( $guid ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
preg_match( '/^[a-z0-9]{8}$/i', $guid, $matches );
|
||||
|
||||
if ( empty( $matches ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax method that is used by the VideoPress player to get a token to play a video.
|
||||
*
|
||||
* This is used for both logged in and logged out users.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function wp_ajax_videopress_get_playback_jwt() {
|
||||
$guid = filter_input( INPUT_POST, 'guid' );
|
||||
$embedded_post_id = filter_input( INPUT_POST, 'post_id', FILTER_VALIDATE_INT );
|
||||
$selected_plan_id = filter_input( INPUT_POST, 'subscription_plan_id' );
|
||||
|
||||
if ( empty( $embedded_post_id ) ) {
|
||||
$embedded_post_id = 0;
|
||||
}
|
||||
|
||||
if ( empty( $guid ) || ! $this->is_valid_guid( $guid ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'need a guid', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->is_current_user_authed_for_video( $guid, $embedded_post_id, $selected_plan_id ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'You cannot view this video.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->request_jwt_from_wpcom( $guid );
|
||||
|
||||
if ( empty( $token ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress playback JWT. Please try again later. (empty upload token)', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload JWT. Please try again later.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_success( array( 'jwt' => $token ), null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user can view the provided video. Only ever gets fired if site-wide private videos are enabled.
|
||||
*
|
||||
* Filterable for 3rd party plugins.
|
||||
*
|
||||
* @param string $guid The video id being checked.
|
||||
* @param int $embedded_post_id The post id the video is embedded in or 0.
|
||||
* @param int $selected_plan_id The plan id the earn block this video is embedded in has.
|
||||
*/
|
||||
private function is_current_user_authed_for_video( $guid, $embedded_post_id, $selected_plan_id = 0 ) {
|
||||
return Access_Control::instance()->is_current_user_authed_for_video( $guid, $embedded_post_id, $selected_plan_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests JWT from wpcom.
|
||||
*
|
||||
* @param string $guid The video id being checked.
|
||||
*/
|
||||
private function request_jwt_from_wpcom( $guid ) {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'video_wpcom_get_playback_jwt_for_guid' ) ) {
|
||||
$jwt_data = video_wpcom_get_playback_jwt_for_guid( $guid );
|
||||
if ( is_wp_error( $jwt_data ) ) {
|
||||
return false;
|
||||
}
|
||||
return $jwt_data->metadata_token;
|
||||
}
|
||||
|
||||
$video_blog_id = $this->get_videopress_blog_id();
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
);
|
||||
|
||||
$endpoint = "sites/{$video_blog_id}/media/videopress-playback-jwt/{$guid}";
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, 'v2', $args, null, 'wpcom' );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['metadata_token'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['metadata_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax method that is used by the VideoPress uploader to get a token to upload a file to the wpcom api.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function wp_ajax_videopress_get_upload_jwt() {
|
||||
if ( ! current_user_can( 'upload_files' ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to upload files.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$video_blog_id = $this->get_videopress_blog_id();
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
);
|
||||
|
||||
$endpoint = "sites/{$video_blog_id}/media/videopress-upload-jwt";
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, 'v2', $args, null, 'wpcom' );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload JWT. Please try again later.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['upload_token'] ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload JWT. Please try again later. (empty upload token)', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$response['upload_action_url'] = videopress_make_resumable_upload_path( $video_blog_id );
|
||||
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_success( $response, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax method that is used by the VideoPress uploader to get a token to upload a file to the wpcom api.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function wp_ajax_videopress_get_upload_token() {
|
||||
if ( ! current_user_can( 'upload_files' ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to upload files.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$video_blog_id = $this->get_videopress_blog_id();
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
);
|
||||
|
||||
$endpoint = "sites/{$video_blog_id}/media/token";
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $args );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['upload_token'] ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$response['upload_action_url'] = videopress_make_media_upload_path( $video_blog_id );
|
||||
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_success( $response, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax action to update the video transcoding status from the WPCOM API.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function wp_ajax_update_transcoding_status() {
|
||||
if ( ! isset( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Informational AJAX response.
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'A valid post_id is required.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
$post_id = (int) $_POST['post_id']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( ! videopress_update_meta_data( $post_id ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( array( 'message' => __( 'That post does not have a VideoPress video associated to it.', 'jetpack-videopress-pkg' ) ), null, JSON_UNESCAPED_SLASHES );
|
||||
return;
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Status updated', 'jetpack-videopress-pkg' ),
|
||||
'status' => videopress_get_transcoding_status( $post_id ),
|
||||
),
|
||||
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proper blog id depending on Jetpack or WP.com
|
||||
*
|
||||
* @return int the blog id
|
||||
*/
|
||||
public function get_videopress_blog_id() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
return get_current_blog_id();
|
||||
}
|
||||
|
||||
$options = Options::get_options();
|
||||
return $options['shadow_blog_id'];
|
||||
}
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Attachment_Handler
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Current_Plan;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* VideoPress Attachment_Handler class.
|
||||
*/
|
||||
class Attachment_Handler {
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
* This method should be called only once by the Initializer class. Do not call this method again.
|
||||
*/
|
||||
public static function init() {
|
||||
|
||||
if ( ! Status::is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'wp_get_attachment_url', array( __CLASS__, 'maybe_get_attached_url_for_videopress' ), 10, 2 );
|
||||
add_filter( 'get_attached_file', array( __CLASS__, 'maybe_get_attached_url_for_videopress' ), 10, 2 );
|
||||
|
||||
if ( Current_Plan::supports( 'videopress' ) ) {
|
||||
add_filter( 'upload_mimes', array( __CLASS__, 'add_video_upload_mimes' ), 999 );
|
||||
}
|
||||
|
||||
add_filter( 'pre_delete_attachment', array( __CLASS__, 'delete_video_wpcom' ), 10, 2 );
|
||||
add_filter( 'wp_mime_type_icon', array( __CLASS__, 'wp_mime_type_icon' ), 10, 3 );
|
||||
add_filter( 'wp_video_extensions', array( __CLASS__, 'add_videopress_extenstion' ) );
|
||||
|
||||
add_filter( 'wp_prepare_attachment_for_js', array( __CLASS__, 'prepare_attachment_for_js' ) );
|
||||
add_filter( 'ajax_query_attachments_args', array( __CLASS__, 'ajax_query_attachments_args' ) );
|
||||
add_action( 'pre_get_posts', array( __CLASS__, 'media_list_table_query' ) );
|
||||
|
||||
add_filter( 'user_has_cap', array( __CLASS__, 'disable_delete_if_disconnected' ), 10, 3 );
|
||||
|
||||
add_action( 'admin_print_scripts-upload.php', array( __CLASS__, 'enqueue_media_library_poll' ) );
|
||||
add_action( 'admin_print_styles-upload.php', array( __CLASS__, 'enqueue_media_library_styles' ) );
|
||||
add_filter( 'heartbeat_received', array( __CLASS__, 'heartbeat_received' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the VideoPress URL for the give post id, otherwise returns the provided default.
|
||||
*
|
||||
* This is an attachment-based filter handler.
|
||||
*
|
||||
* @param string $default The default return value if post id is not a VideoPress video.
|
||||
* @param int $post_id The post id for the current attachment.
|
||||
*/
|
||||
public static function maybe_get_attached_url_for_videopress( $default, $post_id ) {
|
||||
$videopress_url = videopress_get_attachment_url( $post_id );
|
||||
|
||||
if ( null !== $videopress_url ) {
|
||||
return $videopress_url;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that all video mimes are added in, as multi site installs can remove them.
|
||||
*
|
||||
* @param array $existing_mimes Mime types to extend/filter.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_video_upload_mimes( $existing_mimes = array() ) {
|
||||
$mime_types = wp_get_mime_types();
|
||||
$video_types = array_filter( $mime_types, array( __CLASS__, 'filter_video_mimes' ) );
|
||||
|
||||
foreach ( $video_types as $key => $value ) {
|
||||
$existing_mimes[ $key ] = $value;
|
||||
}
|
||||
|
||||
// Make sure that videopress mimes are considered videos.
|
||||
$existing_mimes['videopress'] = 'video/videopress';
|
||||
|
||||
return $existing_mimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter designed to get rid of non video mime types.
|
||||
*
|
||||
* @param string $value Mime type to filter.
|
||||
* @return int
|
||||
*/
|
||||
public static function filter_video_mimes( $value ) {
|
||||
return preg_match( '@^video/@', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to delete a VideoPress video from wp.com.
|
||||
* Will block the deletion from continuing if certain errors return from the wp.com API.
|
||||
*
|
||||
* @param Boolean $delete if the deletion should occur or not (unused).
|
||||
* @param WP_Post $post the post object.
|
||||
*
|
||||
* @return null|WP_Error|Boolean null if deletion should continue.
|
||||
*/
|
||||
public static function delete_video_wpcom( $delete, $post ) {
|
||||
if ( ! is_videopress_attachment( $post->ID ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$guid = get_post_meta( $post->ID, 'videopress_guid', true );
|
||||
if ( empty( $guid ) ) {
|
||||
self::delete_video_poster_attachment( $post->ID );
|
||||
return null;
|
||||
}
|
||||
|
||||
// Phone home and have wp.com delete the VideoPress entry and files.
|
||||
$wpcom_response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/videos/%s/delete', $guid ),
|
||||
'1.1',
|
||||
array( 'method' => 'POST' ),
|
||||
array( 'user_id' => get_current_user_id() )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $wpcom_response ) ) {
|
||||
return $wpcom_response;
|
||||
}
|
||||
|
||||
// Upon success or a 404 (video already deleted on wp.com), return null to allow the deletion to continue.
|
||||
if ( 200 === $wpcom_response['response']['code'] || 404 === $wpcom_response['response']['code'] ) {
|
||||
self::delete_video_poster_attachment( $post->ID );
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise we stop the deletion from proceeding.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a video poster attachment if it exists.
|
||||
*
|
||||
* @param int $attachment_id the WP attachment id.
|
||||
*/
|
||||
private static function delete_video_poster_attachment( $attachment_id ) {
|
||||
$thumbnail_id = get_post_meta( $attachment_id, '_thumbnail_id', true );
|
||||
if ( ! empty( $thumbnail_id ) ) {
|
||||
// Let's ensure this is a VP poster image before we delete it.
|
||||
if ( '1' === get_post_meta( $thumbnail_id, 'videopress_poster_image', true ) ) {
|
||||
// This call triggers the `delete_video_wpcom` filter again but it bails early at the is_videopress_attachment() check.
|
||||
wp_delete_attachment( $thumbnail_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the mime type icon.
|
||||
*
|
||||
* @param string $icon Icon path.
|
||||
* @param string $mime Mime type.
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function wp_mime_type_icon( $icon, $mime, $post_id ) {
|
||||
|
||||
if ( $mime !== 'video/videopress' ) {
|
||||
return $icon;
|
||||
}
|
||||
|
||||
$status = get_post_meta( $post_id, 'videopress_status', true );
|
||||
|
||||
if ( $status === 'complete' ) {
|
||||
return $icon;
|
||||
}
|
||||
|
||||
return 'https://wordpress.com/wp-content/mu-plugins/videopress/images/media-video-processing-icon.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of supported video formats.
|
||||
*
|
||||
* @param array $extensions Supported video formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function add_videopress_extenstion( $extensions ) {
|
||||
$extensions[] = 'videopress';
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that any Video that has a VideoPress GUID passes that data back.
|
||||
*
|
||||
* @param array $post Attachment data array.
|
||||
* @return array
|
||||
*/
|
||||
public static function prepare_attachment_for_js( $post ) {
|
||||
if ( 'video' === $post['type'] ) {
|
||||
$guid = get_post_meta( $post['id'], 'videopress_guid', true );
|
||||
if ( $guid ) {
|
||||
$post['videopress_guid'] = $guid;
|
||||
}
|
||||
$status = get_post_meta( $post['id'], 'videopress_status', true );
|
||||
if ( $status ) {
|
||||
$post['videopress_status'] = $status;
|
||||
}
|
||||
}
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Grid:
|
||||
* Filter out any videopress video posters that we've downloaded,
|
||||
* so that they don't seem to display twice.
|
||||
*
|
||||
* @param array $args Query variables.
|
||||
*/
|
||||
public static function ajax_query_attachments_args( $args ) {
|
||||
$meta_query = array(
|
||||
array(
|
||||
'key' => 'videopress_poster_image',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
);
|
||||
|
||||
// If there was already a meta query, let's AND it via
|
||||
// nesting it with our new one. No need to specify the
|
||||
// relation, as it defaults to AND.
|
||||
if ( ! empty( $args['meta_query'] ) ) {
|
||||
$meta_query[] = $args['meta_query'];
|
||||
}
|
||||
$args['meta_query'] = $meta_query;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media List:
|
||||
* Do the same as `videopress_ajax_query_attachments_args()` but for the list view.
|
||||
*
|
||||
* @param array $query WP_Query instance.
|
||||
*/
|
||||
public static function media_list_table_query( $query ) {
|
||||
|
||||
if (
|
||||
! function_exists( 'get_current_screen' )
|
||||
|| get_current_screen() === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_admin() && $query->is_main_query() && ( 'upload' === get_current_screen()->id ) ) {
|
||||
$meta_query = array(
|
||||
array(
|
||||
'key' => 'videopress_poster_image',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
);
|
||||
|
||||
$old_meta_query = $query->get( 'meta_query' );
|
||||
if ( $old_meta_query ) {
|
||||
$meta_query[] = $old_meta_query;
|
||||
}
|
||||
|
||||
$query->set( 'meta_query', $meta_query );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to disable the `delete_post` capability
|
||||
* for VideoPress attachments if the current user is
|
||||
* not connected.
|
||||
*
|
||||
* @param array $allcaps All the capabilities of the user.
|
||||
* @param array $cap [0] Required capability.
|
||||
* @param array $args [0] Requested capability.
|
||||
* [1] User ID.
|
||||
* [2] Associated object ID.
|
||||
* @return array the filtered array of capabilities.
|
||||
*/
|
||||
public static function disable_delete_if_disconnected( $allcaps, $cap, $args ) {
|
||||
|
||||
// Only apply this filter to `delete_post` checks
|
||||
if ( ! isset( $args[0] ) || 'delete_post' !== $args[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
// Only apply this filter to VideoPress attachments
|
||||
if ( ! isset( $args[2] ) || ! is_videopress_attachment( $args[2] ) ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
// Set the capability to false if the user can't perform the actions
|
||||
if ( ! Data::can_perform_action() ) {
|
||||
$allcaps[ $cap[0] ] = false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the script that hooks into WP Heartbeat to refresh
|
||||
* processing VideoPress video data in the media library grid.
|
||||
*
|
||||
* @since 0.35.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enqueue_media_library_poll() {
|
||||
wp_enqueue_script(
|
||||
'videopress-media-library-poll',
|
||||
plugins_url( 'js/media-library-poll.js', __FILE__ ),
|
||||
array( 'jquery', 'heartbeat', 'media-grid' ),
|
||||
Package_Version::PACKAGE_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
add_filter( 'heartbeat_settings', array( __CLASS__, 'heartbeat_settings' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds inline styles for the media library grid on the upload page.
|
||||
*
|
||||
* @since 0.35.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enqueue_media_library_styles() {
|
||||
// Constrain poster images so they fit within the media library grid cell.
|
||||
wp_add_inline_style(
|
||||
'media-views',
|
||||
'.attachment-preview.type-video .thumbnail .centered img.thumbnail { max-width: 100%; max-height: 100%; }'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowers the Heartbeat minimum interval on the media library page
|
||||
* so the polling script can request a faster tick rate.
|
||||
*
|
||||
* @since 0.35.4
|
||||
*
|
||||
* @param array $settings Heartbeat settings.
|
||||
* @return array
|
||||
*/
|
||||
public static function heartbeat_settings( $settings ) {
|
||||
$settings['minimalInterval'] = 5;
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat API handler that checks the processing status of VideoPress videos.
|
||||
*
|
||||
* @since 0.35.4
|
||||
*
|
||||
* @param array $response The Heartbeat response.
|
||||
* @param array $data The data sent with the Heartbeat request.
|
||||
* @return array
|
||||
*/
|
||||
public static function heartbeat_received( $response, $data ) {
|
||||
if ( empty( $data['videopress_processing_ids'] ) || ! is_array( $data['videopress_processing_ids'] ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$statuses = array();
|
||||
foreach ( $data['videopress_processing_ids'] as $id ) {
|
||||
$id = (int) $id;
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = get_post_meta( $id, 'videopress_status', true );
|
||||
if ( $status ) {
|
||||
$statuses[ $id ] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $statuses ) ) {
|
||||
$response['videopress_processing_status'] = $statuses;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Block Editor Content
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* VideoPress block editor class for content generation
|
||||
*/
|
||||
class Block_Editor_Content {
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
* This method should be called only once by the Initializer class. Do not call this method again.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! Status::is_standalone_plugin_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the videopress shortcodes added by the Jetpack plugin.
|
||||
if ( shortcode_exists( 'videopress' ) ) {
|
||||
remove_shortcode( 'videopress' );
|
||||
}
|
||||
if ( shortcode_exists( 'wpvideo' ) ) {
|
||||
remove_shortcode( 'wpvideo' );
|
||||
}
|
||||
|
||||
add_shortcode( 'videopress', array( static::class, 'videopress_embed_shortcode' ) );
|
||||
add_shortcode( 'wpvideo', array( static::class, 'videopress_embed_shortcode' ) );
|
||||
|
||||
add_filter( 'wp_video_shortcode_override', array( static::class, 'video_shortcode_override' ), 10, 4 );
|
||||
|
||||
add_filter( 'default_content', array( static::class, 'videopress_video_block_by_guid' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* VideoPress embed shortcode
|
||||
*
|
||||
* Expected input format:
|
||||
* [videopress tLvEwHYZ]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
*
|
||||
* @return string html
|
||||
*/
|
||||
public static function videopress_embed_shortcode( $atts ) {
|
||||
/**
|
||||
* We only accept GUIDs as a first unnamed argument.
|
||||
*/
|
||||
$guid = $atts[0] ?? null;
|
||||
|
||||
/**
|
||||
* Make sure the GUID passed in matches how actual GUIDs are formatted.
|
||||
*/
|
||||
if ( ! videopress_is_valid_guid( $guid ) ) {
|
||||
return '<!-- error: missing or invalid VideoPress video ID -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the defaults
|
||||
*/
|
||||
$defaults = array(
|
||||
'w' => 640, // Width of the video player, in pixels
|
||||
'h' => 0, // Height of the video player, in pixels
|
||||
'at' => 0, // How many seconds in to initially seek to
|
||||
'loop' => false, // Whether to loop the video repeatedly
|
||||
'autoplay' => false, // Whether to autoplay the video on load
|
||||
'cover' => true, // Whether to scale the video to its container
|
||||
'muted' => false, // Whether the video should start without sound
|
||||
'controls' => true, // Whether the video should display controls
|
||||
'playsinline' => false, // Whether the video should be allowed to play inline (for browsers that support this)
|
||||
'useaveragecolor' => false, // Whether the video should use the seekbar automatic average color
|
||||
'preloadcontent' => 'metadata',
|
||||
);
|
||||
|
||||
// Make sure "false" will be actually false.
|
||||
foreach ( $atts as $key => $value ) {
|
||||
if ( is_string( $value ) && 'false' === strtolower( $value ) ) {
|
||||
$atts[ $key ] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $atts['preload'] ) && videopress_is_valid_preload( $atts['preload'] ) ) {
|
||||
$atts['preloadcontent'] = $atts['preload'];
|
||||
}
|
||||
|
||||
if ( isset( $atts['preloadcontent'] ) && ! videopress_is_valid_preload( $atts['preloadcontent'] ) ) {
|
||||
unset( $atts['preloadcontent'] );
|
||||
}
|
||||
|
||||
$atts = shortcode_atts( $defaults, $atts, 'videopress' );
|
||||
|
||||
$base_url = 'https://videopress.com/embed/' . $guid;
|
||||
$query_params = array(
|
||||
'at' => $atts['at'],
|
||||
'loop' => $atts['loop'],
|
||||
'autoplay' => $atts['autoplay'],
|
||||
'muted' => $atts['muted'],
|
||||
'controls' => $atts['controls'],
|
||||
'playsinline' => $atts['playsinline'],
|
||||
'useAverageColor' => $atts['useaveragecolor'], // The casing is intentional, shortcode params are lowercase, but player expects useAverageColor
|
||||
'preloadContent' => $atts['preloadcontent'], // The casing is intentional, shortcode params are lowercase, but player expects preloadContent
|
||||
);
|
||||
$src = esc_url( add_query_arg( $query_params, $base_url ) );
|
||||
|
||||
$width = absint( $atts['w'] );
|
||||
if ( ! $atts['h'] ) {
|
||||
$aspect_ratio = 16 / 9; // TODO: Get the correct aspect ratio for the video.
|
||||
$height = $width / $aspect_ratio;
|
||||
} else {
|
||||
$height = absint( $atts['h'] );
|
||||
}
|
||||
|
||||
$cover = $atts['cover'] ? ' data-resize-to-parent="true"' : '';
|
||||
|
||||
$block_template =
|
||||
'<figure class="wp-block-videopress-video wp-block-jetpack-videopress jetpack-videopress-player">' .
|
||||
'<div class="jetpack-videopress-player__wrapper">' .
|
||||
'<iframe ' .
|
||||
'title="' . __( 'VideoPress Video Player', 'jetpack-videopress-pkg' ) . '" ' .
|
||||
'aria-label="' . __( 'VideoPress Video Player', 'jetpack-videopress-pkg' ) . '" ' .
|
||||
'src="%s" ' .
|
||||
'width="%s"' .
|
||||
'height="%s" ' .
|
||||
'frameborder="0" ' .
|
||||
'allowfullscreen%s allow="clipboard-write">' .
|
||||
'</iframe>' .
|
||||
'</div>' .
|
||||
'</figure>';
|
||||
|
||||
$version = Package_Version::PACKAGE_VERSION;
|
||||
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
|
||||
wp_enqueue_script( 'videopress-iframe', 'https://videopress.com/videopress-iframe.js', array(), $version, true );
|
||||
|
||||
return sprintf( $block_template, $src, $width, $height, $cover );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a VideoPress video block content with the given guid
|
||||
*
|
||||
* @param string $content Post content.
|
||||
* @param WP_Post $post Post.
|
||||
* @return string
|
||||
*/
|
||||
public static function videopress_video_block_by_guid( $content, $post ) {
|
||||
if ( isset( $_GET['videopress_guid'] ) && isset( $_GET['_wpnonce'] )
|
||||
&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'videopress-content-nonce' )
|
||||
&& current_user_can( 'edit_post', $post->ID )
|
||||
&& '' === $content
|
||||
) {
|
||||
$guid = sanitize_text_field( wp_unslash( $_GET['videopress_guid'] ) );
|
||||
|
||||
$base_url = 'https://videopress.com/v/' . $guid;
|
||||
$query_params = array(
|
||||
'resizeToParent' => 'true',
|
||||
'cover' => 'true',
|
||||
'preloadContent' => 'metadata',
|
||||
'useAverageColor' => 'true',
|
||||
);
|
||||
$url = esc_url( add_query_arg( $query_params, $base_url ) );
|
||||
|
||||
if ( ! empty( $guid ) ) {
|
||||
// ref /client/lib/url/index.ts
|
||||
$content = '<!-- wp:videopress/video {"guid":"' . $guid . '"} -->
|
||||
<figure class="wp-block-videopress-video wp-block-jetpack-videopress jetpack-videopress-player">
|
||||
<div class="jetpack-videopress-player__wrapper">' . $url . '</div>
|
||||
</figure>
|
||||
<!-- /wp:videopress/video -->';
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the standard video short tag to also process videopress files as well.
|
||||
*
|
||||
* This will parse the given src and, if it is a videopress file, parse as the
|
||||
* VideoPress shortcode instead.
|
||||
*
|
||||
* @param string $html Empty variable to be replaced with shortcode markup.
|
||||
* @param array $attr Attributes of the video shortcode.
|
||||
* @param string $content Video shortcode content.
|
||||
* @param int $instance Unique numeric ID of this video shortcode instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function video_shortcode_override( $html, $attr, $content, $instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$videopress_guid = null;
|
||||
|
||||
if ( isset( $attr['videopress_guid'] ) ) {
|
||||
$videopress_guid = $attr['videopress_guid'];
|
||||
} else {
|
||||
// Handle the different possible url attributes
|
||||
$url_keys = array( 'src', 'mp4' );
|
||||
|
||||
foreach ( $url_keys as $key ) {
|
||||
if ( isset( $attr[ $key ] ) ) {
|
||||
$url = $attr[ $key ];
|
||||
// phpcs:ignore WordPress.WP.CapitalPDangit
|
||||
if ( preg_match( '@videos.(videopress\.com|files\.wordpress\.com)/([a-z0-9]{8})/@i', $url, $matches ) ) {
|
||||
$videopress_guid = $matches[2];
|
||||
}
|
||||
|
||||
// Also test for videopress oembed url, which is used by the Video Media Widget.
|
||||
if ( ! $videopress_guid && preg_match( '@https://videopress.com/v/([a-z0-9]{8})@i', $url, $matches ) ) {
|
||||
$videopress_guid = $matches[1];
|
||||
}
|
||||
|
||||
// Also test for old v.wordpress.com oembed URL.
|
||||
if ( ! $videopress_guid && preg_match( '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|i', $url, $matches ) ) { // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
|
||||
$videopress_guid = $matches[1];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $videopress_guid ) {
|
||||
$videopress_atts = array( $videopress_guid );
|
||||
|
||||
// height is ignored on jetpack video block, so we don't pass it for consistency.
|
||||
if ( isset( $attr['width'] ) ) {
|
||||
$videopress_atts['w'] = (int) $attr['width'];
|
||||
}
|
||||
if ( isset( $attr['muted'] ) ) {
|
||||
$videopress_atts['muted'] = $attr['muted'];
|
||||
}
|
||||
if ( isset( $attr['autoplay'] ) ) {
|
||||
$videopress_atts['autoplay'] = $attr['autoplay'];
|
||||
}
|
||||
if ( isset( $attr['loop'] ) ) {
|
||||
$videopress_atts['loop'] = $attr['loop'];
|
||||
}
|
||||
// The core video block doesn't support the cover attribute, setting it to false for consistency.
|
||||
$videopress_atts['cover'] = false;
|
||||
|
||||
// Then display the VideoPress version of the stored GUID!
|
||||
return self::videopress_embed_shortcode( $videopress_atts );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Extensions
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* VideoPress Extensions class.
|
||||
*/
|
||||
class Block_Editor_Extensions {
|
||||
/**
|
||||
* What version of the blocks we are loading.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $blocks_variation = 'production';
|
||||
|
||||
/**
|
||||
* Script handle
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $script_handle = '';
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
* This method should be called only once by the Block registrar.
|
||||
* Do not call this method again.
|
||||
*
|
||||
* @param string $script_handle - The script handle.
|
||||
*/
|
||||
public static function init( $script_handle ) {
|
||||
if ( ! Status::is_registrant_plugin_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use the videopress/video editor script handle to localize enqueue scripts.
|
||||
* @see https://developer.wordpress.org/reference/functions/generate_block_asset_handle
|
||||
*/
|
||||
self::$script_handle = $script_handle;
|
||||
|
||||
/**
|
||||
* Alternative to `JETPACK_BETA_BLOCKS`, set to `true` to load Beta Blocks.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @deprecated Jetpack 11.8.0 Use jetpack_blocks_variation filter instead.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
if (
|
||||
apply_filters_deprecated(
|
||||
'jetpack_load_beta_blocks',
|
||||
array( false ),
|
||||
'jetpack-11.8.0',
|
||||
'jetpack_blocks_variation'
|
||||
)
|
||||
) {
|
||||
self::$blocks_variation = 'beta';
|
||||
}
|
||||
|
||||
/*
|
||||
* Get block variation, from the new constant or the old one.
|
||||
*/
|
||||
if ( Constants::is_true( 'JETPACK_BETA_BLOCKS' ) ) {
|
||||
self::$blocks_variation = 'beta';
|
||||
}
|
||||
|
||||
$blocks_variation = Constants::get_constant( 'JETPACK_BLOCKS_VARIATION' );
|
||||
if ( ! empty( $blocks_variation ) ) {
|
||||
/**
|
||||
* Allow customizing the variation of blocks in use on a site.
|
||||
* Overwrites any previously set values, whether by constant or filter.
|
||||
*
|
||||
* @since Jetpack 8.1.0
|
||||
*
|
||||
* @param string $block_variation Can be beta, experimental, and production. Defaults to production.
|
||||
*/
|
||||
self::$blocks_variation = apply_filters( 'jetpack_blocks_variation', $blocks_variation );
|
||||
}
|
||||
|
||||
// Register the script.
|
||||
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_extensions' ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the extensions list
|
||||
*
|
||||
* @return array The extensions list.
|
||||
*/
|
||||
public static function get_list() {
|
||||
$videopress_extensions_file = __DIR__ . '/../build/block-editor/extensions/index.json';
|
||||
$videopress_extensions_file_exists = file_exists( $videopress_extensions_file );
|
||||
if ( ! $videopress_extensions_file_exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$videopress_extensions_data = (array) json_decode(
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
file_get_contents( $videopress_extensions_file )
|
||||
);
|
||||
|
||||
$extensions_list = array_map(
|
||||
function ( $extension ) {
|
||||
return array(
|
||||
'name' => $extension,
|
||||
'isBeta' => true,
|
||||
'isEnabled' => 'beta' === self::$blocks_variation,
|
||||
);
|
||||
},
|
||||
$videopress_extensions_data['beta']
|
||||
);
|
||||
|
||||
return $extensions_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the extension is available
|
||||
*
|
||||
* @param string $slug The extension slug.
|
||||
* @return boolean True if the extension is available, false otherwise.
|
||||
*/
|
||||
public static function is_extension_available( $slug ) {
|
||||
$extensions_list = self::get_list();
|
||||
foreach ( $extensions_list as $extension ) {
|
||||
if ( $extension['name'] === $slug ) {
|
||||
return $extension['isEnabled'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the extensions script.
|
||||
*/
|
||||
public static function enqueue_extensions() {
|
||||
$extensions_list = self::get_list();
|
||||
|
||||
$site_type = 'jetpack';
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$site_type = 'simple';
|
||||
} elseif ( ( new Host() )->is_woa_site() ) {
|
||||
$site_type = 'atomic';
|
||||
}
|
||||
|
||||
$videopress_editor_state = array(
|
||||
'extensions' => $extensions_list,
|
||||
'siteType' => $site_type,
|
||||
'myJetpackConnectUrl' => admin_url( 'admin.php?page=my-jetpack#/connection' ),
|
||||
'jetpackVideoPressSettingUrl' => admin_url( 'admin.php?page=jetpack#/settings?term=videopress' ),
|
||||
'isVideoPressModuleActive' => Status::is_jetpack_plugin_and_videopress_module_active(),
|
||||
'isStandaloneActive' => Status::is_standalone_plugin_active(),
|
||||
'imagesURLBase' => plugin_dir_url( __DIR__ ) . 'build/images/',
|
||||
'playerBridgeUrl' => plugins_url( '../build/lib/player-bridge.js', __FILE__ ),
|
||||
);
|
||||
|
||||
// Expose initial state of site connection
|
||||
Connection_Initial_State::render_script( self::$script_handle );
|
||||
|
||||
// Expose initial state of videoPress editor
|
||||
wp_localize_script( self::$script_handle, 'videoPressEditorState', $videopress_editor_state );
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Media block replacement class.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
**/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* Class Block_Replacement
|
||||
**/
|
||||
class Block_Replacement {
|
||||
/**
|
||||
* Whether the class has been initiated.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $initiated = false;
|
||||
|
||||
/**
|
||||
* Initialize replacement.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$initiated ) {
|
||||
return;
|
||||
}
|
||||
add_filter( 'render_block', array( self::class, 'replace_media_text_with_videopress' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace video in Media & Text block with Videopress shortcode.
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
* @param array $block The block.
|
||||
* @return string
|
||||
*/
|
||||
public static function replace_media_text_with_videopress( $block_content, $block ) {
|
||||
if ( isset( $block['blockName'] ) && $block['blockName'] === 'core/media-text' ) {
|
||||
|
||||
// Make sure we have a $post_id that could be valid; if we don't, then video_get_info_by_blogpostid()
|
||||
// will fail, so there's no point in calling it.
|
||||
$post_id = isset( $block['attrs']['mediaId'] ) ? (int) $block['attrs']['mediaId'] : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
$video_info = video_get_info_by_blogpostid( get_current_blog_id(), $post_id );
|
||||
if ( $video_info && $video_info->guid ) {
|
||||
$videopress_shortcode = sprintf( '[videopress %s]', esc_attr( $video_info->guid ) );
|
||||
$block_content = preg_replace( '/<video.*?<\/video>/is', do_shortcode( $videopress_shortcode ), $block_content );
|
||||
}
|
||||
}
|
||||
return $block_content;
|
||||
}
|
||||
}
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
/**
|
||||
* The Data class.
|
||||
* This class provides methods for data VideoPress access and manipulation.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use WP_REST_Request;
|
||||
/**
|
||||
* The Data class.
|
||||
*/
|
||||
class Data {
|
||||
|
||||
/**
|
||||
* Gets the Jetpack blog ID
|
||||
*
|
||||
* @return int The blog ID
|
||||
*/
|
||||
public static function get_blog_id() {
|
||||
return VideoPressToken::blog_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress site privacy configuration.
|
||||
*
|
||||
* @return boolean If all the videos are private on the site
|
||||
*/
|
||||
public static function get_videopress_videos_private_for_site() {
|
||||
/**
|
||||
* If it's a Simple site, returns the site privacy setting.
|
||||
*/
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
return video_is_private_wpcom_blog( get_current_blog_id() );
|
||||
}
|
||||
/**
|
||||
* If it's a private Atomic site, the default setting is private as well.
|
||||
*/
|
||||
if ( ( new Host() )->is_woa_site() ) {
|
||||
if ( ( intval( get_option( 'blog_public', '' ) ) === -1 ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* If it's a Jetpack site or a public Atomic site, check the settings */
|
||||
return boolval( get_option( 'videopress_private_enabled_for_site', false ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress Settings.
|
||||
*
|
||||
* @return array The settings as an associative array.
|
||||
*/
|
||||
public static function get_videopress_settings() {
|
||||
$site_type = 'jetpack';
|
||||
$site_is_private = false;
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$site_type = 'simple';
|
||||
$site_is_private = video_is_private_wpcom_blog( get_current_blog_id() );
|
||||
} elseif ( ( new Host() )->is_woa_site() ) {
|
||||
$site_type = 'atomic';
|
||||
$site_is_private = intval( get_option( 'blog_public', '' ) ) === -1;
|
||||
}
|
||||
|
||||
return array(
|
||||
'videopress_videos_private_for_site' => self::get_videopress_videos_private_for_site(),
|
||||
'site_is_private' => $site_is_private,
|
||||
'site_type' => $site_type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the video data
|
||||
*
|
||||
* @param boolean $is_videopress - True when getting VideoPress data.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_video_data( $is_videopress = true ) {
|
||||
$video_data = array(
|
||||
'videos' => array(),
|
||||
'total' => 0,
|
||||
'totalPages' => 0,
|
||||
'query' => array(
|
||||
'order' => 'desc',
|
||||
'orderBy' => 'date',
|
||||
'itemsPerPage' => 6,
|
||||
'page' => 1,
|
||||
),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'order' => $video_data['query']['order'],
|
||||
'orderby' => $video_data['query']['orderBy'],
|
||||
'per_page' => $video_data['query']['itemsPerPage'],
|
||||
'page' => $video_data['query']['page'],
|
||||
);
|
||||
|
||||
if ( $is_videopress ) {
|
||||
$args['mime_type'] = 'video/videopress';
|
||||
} else {
|
||||
$args['media_type'] = 'video';
|
||||
$args['no_videopress'] = true;
|
||||
}
|
||||
|
||||
// Do an internal request for the media list
|
||||
$request = new WP_REST_Request( 'GET', '/wp/v2/media' );
|
||||
$request->set_query_params( $args );
|
||||
$response = rest_do_request( $request );
|
||||
|
||||
if ( $response->is_error() ) {
|
||||
// @todo: error handling
|
||||
return $video_data;
|
||||
}
|
||||
|
||||
// load the real values
|
||||
$video_data['videos'] = $response->get_data();
|
||||
$headers = $response->get_headers();
|
||||
|
||||
if ( isset( $headers['X-WP-Total'] ) ) {
|
||||
$video_data['total'] = $headers['X-WP-Total'];
|
||||
}
|
||||
|
||||
if ( isset( $headers['X-WP-TotalPages'] ) ) {
|
||||
$video_data['totalPages'] = $headers['X-WP-TotalPages'];
|
||||
}
|
||||
|
||||
return $video_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_data() {
|
||||
$user_data = array(
|
||||
'items' => array(),
|
||||
'pagination' => array(
|
||||
'total' => 0,
|
||||
'totalPages' => 1,
|
||||
),
|
||||
'query' => array(
|
||||
'order' => 'asc',
|
||||
'orderBy' => 'name',
|
||||
),
|
||||
'_meta' => array(
|
||||
'relyOnInitialState' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'order' => $user_data['query']['order'],
|
||||
'orderby' => $user_data['query']['orderBy'],
|
||||
);
|
||||
|
||||
// Do an internal request for the user list
|
||||
$request = new WP_REST_Request( 'GET', '/wp/v2/users' );
|
||||
$request->set_query_params( $args );
|
||||
$response = rest_do_request( $request );
|
||||
|
||||
if ( $response->is_error() ) {
|
||||
// @todo: error handling
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
// load the real values
|
||||
$user_data['items'] = $response->get_data();
|
||||
$headers = $response->get_headers();
|
||||
|
||||
if ( isset( $headers['X-WP-Total'] ) ) {
|
||||
$user_data['pagination']['total'] = $headers['X-WP-Total'];
|
||||
}
|
||||
|
||||
if ( isset( $headers['X-WP-TotalPages'] ) ) {
|
||||
$user_data['pagination']['totalPages'] = $headers['X-WP-TotalPages'];
|
||||
}
|
||||
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress used storage space in bytes
|
||||
*
|
||||
* @return int the used storage space
|
||||
*/
|
||||
public static function get_storage_used() {
|
||||
$site_data = Site::get_site_info();
|
||||
if ( is_wp_error( $site_data ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( isset( $site_data['options'] ) && isset( $site_data['options']['videopress_storage_used'] ) ) {
|
||||
return intval( round( $site_data['options']['videopress_storage_used'] * 1024 * 1024 ) );
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the initial state that depends on a valid site connection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_connected_initial_state() {
|
||||
return array(
|
||||
'videos' => array(
|
||||
'storageUsed' => self::get_storage_used(),
|
||||
),
|
||||
'purchases' => array(
|
||||
'items' => Site::get_purchases(),
|
||||
'isFetching' => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the site has as connected owner
|
||||
*/
|
||||
public static function has_connected_owner() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ( new Connection_Manager() )->has_connected_owner() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is able to perform actions that modify data
|
||||
*/
|
||||
public static function can_perform_action() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$connection = new Connection_Manager();
|
||||
|
||||
return (
|
||||
$connection->is_connected() &&
|
||||
self::has_connected_owner() &&
|
||||
$connection->is_user_connected()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the initial state of the VideoPress app,
|
||||
* used to render initially the app in the frontend.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_initial_state() {
|
||||
$videopress_data = self::get_video_data();
|
||||
$local_videos_data = self::get_video_data( false );
|
||||
|
||||
// Tweak local videos data.
|
||||
$local_videos = array_map(
|
||||
function ( $video ) {
|
||||
$video = (array) $video;
|
||||
$id = $video['id'];
|
||||
$media_details = $video['media_details'];
|
||||
$jetpack_videopress = $video['jetpack_videopress'];
|
||||
$read_error = null;
|
||||
|
||||
// In malformed files, the media_details or jetpack_videopress properties are not arrays.
|
||||
if ( ! is_array( $media_details ) || ! is_array( $jetpack_videopress ) ) {
|
||||
$media_details = (array) $media_details;
|
||||
$jetpack_videopress = (array) $jetpack_videopress;
|
||||
$read_error = Upload_Exception::ERROR_MALFORMED_FILE;
|
||||
}
|
||||
|
||||
// Check if video is already uploaded to VideoPress or has some error.
|
||||
try {
|
||||
$uploader = new Uploader( $id );
|
||||
$is_uploaded_to_videopress = $uploader->is_uploaded();
|
||||
} catch ( Upload_Exception $e ) {
|
||||
$is_uploaded_to_videopress = false;
|
||||
$read_error = $e->getCode();
|
||||
}
|
||||
|
||||
$upload_date = $video['date'];
|
||||
$url = $video['source_url'];
|
||||
|
||||
$title = $jetpack_videopress['title'];
|
||||
$description = $jetpack_videopress['description'];
|
||||
$caption = $jetpack_videopress['caption'];
|
||||
|
||||
$width = $media_details['width'] ?? null;
|
||||
$height = $media_details['height'] ?? null;
|
||||
$duration = $media_details['length'] ?? null;
|
||||
|
||||
return array(
|
||||
'id' => $id,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'caption' => $caption,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'url' => $url,
|
||||
'uploadDate' => $upload_date,
|
||||
'duration' => $duration,
|
||||
'isUploadedToVideoPress' => $is_uploaded_to_videopress,
|
||||
'readError' => $read_error,
|
||||
);
|
||||
},
|
||||
$local_videos_data['videos']
|
||||
);
|
||||
|
||||
// Tweak VideoPress videos data.
|
||||
$videos = array_map(
|
||||
array( __CLASS__, 'prepare_videopress_video_data' ),
|
||||
$videopress_data['videos']
|
||||
);
|
||||
|
||||
$site_settings = self::get_videopress_settings();
|
||||
|
||||
$initial_state = array(
|
||||
'users' => self::get_user_data(),
|
||||
'siteSettings' => array(
|
||||
'videoPressVideosPrivateForSite' => $site_settings['videopress_videos_private_for_site'],
|
||||
'siteIsPrivate' => $site_settings['site_is_private'],
|
||||
'siteType' => $site_settings['site_type'],
|
||||
),
|
||||
'videos' => array(
|
||||
'uploadedVideoCount' => $videopress_data['total'],
|
||||
'items' => $videos,
|
||||
'isFetching' => false,
|
||||
'isFetchingUploadedVideoCount' => false,
|
||||
'pagination' => array(
|
||||
'totalPages' => $videopress_data['totalPages'],
|
||||
'total' => $videopress_data['total'],
|
||||
),
|
||||
'query' => $videopress_data['query'],
|
||||
'_meta' => array(
|
||||
'processedAllVideosBeingRemoved' => true,
|
||||
'relyOnInitialState' => true,
|
||||
),
|
||||
),
|
||||
'localVideos' => array(
|
||||
'uploadedVideoCount' => $local_videos_data['total'],
|
||||
'items' => $local_videos,
|
||||
'isFetching' => false,
|
||||
'isFetchingUploadedVideoCount' => false,
|
||||
'pagination' => array(
|
||||
'totalPages' => $local_videos_data['totalPages'],
|
||||
'total' => $local_videos_data['total'],
|
||||
),
|
||||
'query' => $local_videos_data['query'],
|
||||
'_meta' => array(
|
||||
'relyOnInitialState' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( self::has_connected_owner() ) {
|
||||
return array_merge_recursive( $initial_state, self::get_connected_initial_state() );
|
||||
}
|
||||
|
||||
return $initial_state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a single VideoPress video, as returned by the media REST endpoint, to
|
||||
* the shape consumed by the VideoPress dashboard app.
|
||||
*
|
||||
* Videos that are still processing (or otherwise have partial metadata) come
|
||||
* back without `videopress`, `width` or `height` in their media_details, so
|
||||
* every nested access is coalesced to avoid undefined-key and offset-on-null
|
||||
* warnings.
|
||||
*
|
||||
* @param array|object $video A single video entry from the media REST endpoint.
|
||||
* @return array The video data formatted for the dashboard app.
|
||||
*/
|
||||
private static function prepare_videopress_video_data( $video ) {
|
||||
$video = (array) $video;
|
||||
$id = $video['id'];
|
||||
$guid = $video['jetpack_videopress_guid'];
|
||||
$media_details = (array) $video['media_details'];
|
||||
$jetpack_videopress = (array) $video['jetpack_videopress'];
|
||||
|
||||
$videopress_media_details = $media_details['videopress'] ?? array();
|
||||
$width = $media_details['width'] ?? null;
|
||||
$height = $media_details['height'] ?? null;
|
||||
|
||||
$title = $jetpack_videopress['title'];
|
||||
$description = $jetpack_videopress['description'];
|
||||
$caption = $jetpack_videopress['caption'];
|
||||
$rating = $jetpack_videopress['rating'];
|
||||
$allow_download = $jetpack_videopress['allow_download'];
|
||||
$display_embed = $jetpack_videopress['display_embed'];
|
||||
$privacy_setting = $jetpack_videopress['privacy_setting'];
|
||||
$needs_playback_token = $jetpack_videopress['needs_playback_token'];
|
||||
$is_private = $jetpack_videopress['is_private'];
|
||||
|
||||
$original = $videopress_media_details['original'] ?? null;
|
||||
$poster = ( ! $needs_playback_token ) ? ( $videopress_media_details['poster'] ?? null ) : null;
|
||||
$upload_date = $videopress_media_details['upload_date'] ?? null;
|
||||
$duration = $videopress_media_details['duration'] ?? null;
|
||||
$file_url_base = $videopress_media_details['file_url_base'] ?? null;
|
||||
$finished = $videopress_media_details['finished'] ?? null;
|
||||
$files = $videopress_media_details['files'] ?? null;
|
||||
$filename = $original !== null ? basename( $original ) : null;
|
||||
|
||||
if ( isset( $files['dvd']['original_img'] ) && isset( $file_url_base['https'] ) && $privacy_setting !== 1 ) {
|
||||
$thumbnail = $file_url_base['https'] . $files['dvd']['original_img'];
|
||||
} else {
|
||||
$thumbnail = null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $id,
|
||||
'guid' => $guid,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'caption' => $caption,
|
||||
'url' => $original,
|
||||
'uploadDate' => $upload_date,
|
||||
'duration' => $duration,
|
||||
'isPrivate' => $is_private,
|
||||
'posterImage' => $poster,
|
||||
'allowDownload' => $allow_download,
|
||||
'displayEmbed' => $display_embed,
|
||||
'rating' => $rating,
|
||||
'privacySetting' => $privacy_setting,
|
||||
'needsPlaybackToken' => $needs_playback_token,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'poster' => array(
|
||||
'src' => $poster,
|
||||
),
|
||||
'thumbnail' => $thumbnail,
|
||||
'finished' => $finished,
|
||||
'filename' => $filename,
|
||||
);
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Divi integration for Videopress.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
**/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Divi5\Divi_5;
|
||||
|
||||
/**
|
||||
* Class Divi
|
||||
**/
|
||||
class Divi {
|
||||
/**
|
||||
* The instance.
|
||||
*
|
||||
* @var Divi
|
||||
**/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Running or not.
|
||||
*
|
||||
* @var bool
|
||||
**/
|
||||
private $running = false;
|
||||
|
||||
/**
|
||||
* VideoPress Divi Extension object.
|
||||
*
|
||||
* @var ?\VideoPress_Divi_Extension
|
||||
**/
|
||||
private $vidi_extension;
|
||||
|
||||
/**
|
||||
* Initializes VideoPress/Divi integration.
|
||||
*
|
||||
* Called only once by the Initializer class
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function init() {
|
||||
return self::get_instance()->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the extension.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function run() {
|
||||
if ( $this->running ) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->running = true;
|
||||
add_action( 'divi_extensions_init', array( $this, 'initialize_extension' ) );
|
||||
add_action( 'et_fb_framework_loaded', array( $this, 'on_fb_framework_loaded' ) );
|
||||
|
||||
// Divi 5 integration. These hooks only fire under Divi 5; the legacy
|
||||
// extension above continues to serve Divi 4.
|
||||
Divi_5::init();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the Frontend Builder is loaded.
|
||||
*/
|
||||
public function on_fb_framework_loaded() {
|
||||
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the extension's main class instance.
|
||||
*/
|
||||
public function initialize_extension() {
|
||||
require_once plugin_dir_path( __FILE__ ) . 'videopress-divi/class-videopress-divi-extension.php';
|
||||
$this->vidi_extension = new \VideoPress_Divi_Extension();
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
/**
|
||||
* The modernized VideoPress dashboard React initial state.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\My_Jetpack\Product;
|
||||
use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Jetpack_Options;
|
||||
use function admin_url;
|
||||
use function esc_url_raw;
|
||||
use function get_bloginfo;
|
||||
use function get_locale;
|
||||
use function get_option;
|
||||
use function get_site_url;
|
||||
use function plugins_url;
|
||||
use function rest_url;
|
||||
use function wp_add_inline_script;
|
||||
use function wp_create_nonce;
|
||||
use function wp_json_encode;
|
||||
use function wp_parse_url;
|
||||
use function wp_script_is;
|
||||
|
||||
/**
|
||||
* The modernized VideoPress dashboard React initial state.
|
||||
*
|
||||
* Mirrors the Activity Log dashboard pattern: a small structured payload
|
||||
* inlined as `var JPVIDEOPRESS_INITIAL_STATE=...` before the wp-build boot
|
||||
* script runs. Phases 6 and 8 will consume the payload via per-file
|
||||
* `declare const` ambient types.
|
||||
*/
|
||||
class Initial_State {
|
||||
|
||||
const SCRIPT_HANDLE = 'jetpack-videopress-dashboard-wp-admin-prerequisites';
|
||||
|
||||
/**
|
||||
* WPCOM product slug purchased by the dashboard's upgrade CTA.
|
||||
*
|
||||
* Mirrors the product `Plan::get_product()` resolves to
|
||||
* (`$products->jetpack_videopress`). Kept as a constant rather than read
|
||||
* from `Plan::get_product()` because that helper issues a synchronous
|
||||
* WPCOM request, which we don't want to incur on every page render.
|
||||
*/
|
||||
const VIDEOPRESS_PRODUCT_SLUG = 'jetpack_videopress';
|
||||
|
||||
/**
|
||||
* Register the inline-state enqueue hook.
|
||||
*
|
||||
* Hooks `admin_enqueue_scripts` at priority 11 so the wp-build page
|
||||
* (`build/pages/jetpack-videopress-dashboard/page-wp-admin.php`) has
|
||||
* already registered the prerequisites handle at its default priority 10.
|
||||
* The `wp_script_is( ..., 'registered' )` check inside the callback
|
||||
* doubles as the page guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue' ), 11 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline the initial-state payload before the wp-build boot script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enqueue() {
|
||||
if ( ! Admin_UI::is_modernized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( self::SCRIPT_HANDLE, 'registered' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_add_inline_script( self::SCRIPT_HANDLE, ( new self() )->render(), 'before' );
|
||||
|
||||
// Hydrate the JP connection store (`window.JP_CONNECTION_INITIAL_STATE`)
|
||||
// so shared connection-aware hooks — notably
|
||||
// `useProductCheckoutWorkflow`, which powers the upgrade CTA — work on
|
||||
// the modernized dashboard exactly as they do on the legacy one. This
|
||||
// also sets `window.jpTracksContext.blog_id` for `@automattic/jetpack-analytics`.
|
||||
Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initial state data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_data() {
|
||||
$gmt_offset = get_option( 'gmt_offset' );
|
||||
$timezone_string = get_option( 'timezone_string' );
|
||||
$home_host = wp_parse_url( get_site_url(), PHP_URL_HOST );
|
||||
|
||||
return array(
|
||||
'API' => array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'contentNonce' => wp_create_nonce( 'videopress-content-nonce' ),
|
||||
),
|
||||
'jetpackStatus' => array(
|
||||
'calypsoSlug' => ( new Status() )->get_site_suffix(),
|
||||
),
|
||||
'product' => array(
|
||||
// Fed to `useProductCheckoutWorkflow` as the product to purchase.
|
||||
'slug' => self::VIDEOPRESS_PRODUCT_SLUG,
|
||||
),
|
||||
'siteData' => array(
|
||||
'id' => Jetpack_Options::get_option( 'id' ),
|
||||
'title' => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
|
||||
'adminUrl' => esc_url_raw( admin_url() ),
|
||||
'slug' => is_string( $home_host ) ? $home_host : '',
|
||||
'gmtOffset' => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0,
|
||||
'timezoneString' => is_string( $timezone_string ) ? $timezone_string : '',
|
||||
'locale' => str_replace( '_', '-', (string) get_locale() ),
|
||||
// Paid-tier capability check. Drives the free-tier UX (callout /
|
||||
// DataViews configuration) once designer input lands. Backed by
|
||||
// `Product::get_site_features_from_wpcom()`, which caches the WPCOM
|
||||
// `/sites/%d/features` response in a 15-second site transient. On a
|
||||
// cache miss this issues a synchronous WPCOM request that blocks
|
||||
// page rendering until it returns.
|
||||
'hasVideoPressAccess' => self::has_videopress_access(),
|
||||
'isVideoPress1TB' => self::has_videopress_feature( 'videopress-1tb-storage' ),
|
||||
'isVideoPressUnlimited' => self::has_videopress_feature( 'videopress-unlimited-storage' ),
|
||||
),
|
||||
'assets' => array(
|
||||
'buildUrl' => plugins_url( '../build/', __FILE__ ),
|
||||
),
|
||||
// Authoritative map of accepted upload types (extension => mimetype),
|
||||
// so the dashboard's drag-and-drop filter accepts exactly what the
|
||||
// VideoPress backend supports rather than guessing client-side.
|
||||
'allowedVideoExtensions' => Admin_UI::get_allowed_video_extensions(),
|
||||
// Product/pricing payload for the pre-connection upsell. Only
|
||||
// populated when the site isn't connected — the only time the
|
||||
// connection gate renders the upsell — so connected dashboards never
|
||||
// incur the synchronous WPCOM pricing request `get_pricing_data()`
|
||||
// makes.
|
||||
'pricing' => ( new Connection_Manager() )->is_connected() ? null : $this->get_pricing_data(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Product description, feature list, and yearly price for the
|
||||
* pre-connection upsell (a port of the legacy dashboard's pricing table).
|
||||
*
|
||||
* Backed by `Plan::get_product_price()`, which issues a synchronous WPCOM
|
||||
* request, so this only runs for disconnected sites. Returns null when the
|
||||
* product or price data isn't available (e.g. the WPCOM request fails), in
|
||||
* which case the gate falls back to the plain connect screen.
|
||||
*
|
||||
* @return array|null The upsell payload, or null when it can't be built.
|
||||
*/
|
||||
private function get_pricing_data() {
|
||||
$site_product = My_Jetpack_Products::get_product( 'videopress' );
|
||||
$product_price = Plan::get_product_price();
|
||||
|
||||
if ( ! is_array( $site_product ) || ! isset( $product_price['yearly'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => isset( $site_product['description'] ) ? (string) $site_product['description'] : '',
|
||||
'features' => isset( $site_product['features'] ) ? array_values( (array) $site_product['features'] ) : array(),
|
||||
'yearly' => $product_price['yearly'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the site has any paid VideoPress feature flag active.
|
||||
*
|
||||
* Matches the active-features check used by the existing
|
||||
* `videopress/v1/features` REST endpoint: any of the storage tiers
|
||||
* counts as paid access. On WPCOM the legacy `videopress` slug also
|
||||
* grants access.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_videopress_access() {
|
||||
return self::has_videopress_feature( 'videopress-1tb-storage' )
|
||||
|| self::has_videopress_feature( 'videopress-unlimited-storage' )
|
||||
|| ( ( new Host() )->is_wpcom_platform() && self::has_videopress_feature( 'videopress' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the named feature appears in the WPCOM active-features list.
|
||||
*
|
||||
* @param string $feature_slug Feature slug as returned by WPCOM (e.g. `videopress-1tb-storage`).
|
||||
* @return bool
|
||||
*/
|
||||
private static function has_videopress_feature( $feature_slug ) {
|
||||
$features = Product::get_site_features_from_wpcom();
|
||||
|
||||
if ( is_wp_error( $features ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$active = $features['active'] ?? array();
|
||||
|
||||
return in_array( $feature_slug, $active, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state into a JavaScript variable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render() {
|
||||
return 'var JPVIDEOPRESS_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
|
||||
}
|
||||
}
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
/**
|
||||
* The initializer class for the videopress package
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* Initialized the VideoPress package
|
||||
*/
|
||||
class Initializer {
|
||||
|
||||
const JETPACK_VIDEOPRESS_IFRAME_API_HANDLER = 'jetpack-videopress-iframe-api';
|
||||
|
||||
/**
|
||||
* Initialization optinos
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $init_options = array();
|
||||
|
||||
/**
|
||||
* Initializes the VideoPress package
|
||||
*
|
||||
* This method is called by Config::ensure.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! did_action( 'videopress_init' ) ) {
|
||||
|
||||
self::unconditional_initialization();
|
||||
|
||||
if ( Status::is_active() ) {
|
||||
self::active_initialization();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after the VideoPress package is initialized
|
||||
*
|
||||
* @since 0.1.1
|
||||
*/
|
||||
do_action( 'videopress_init' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the initialization options
|
||||
*
|
||||
* This method is called by the Config class
|
||||
*
|
||||
* @param array $options The initialization options.
|
||||
* @return void
|
||||
*/
|
||||
public static function update_init_options( array $options ) {
|
||||
if ( empty( $options['admin_ui'] ) || self::should_initialize_admin_ui() ) { // do not overwrite if already set to true.
|
||||
return;
|
||||
}
|
||||
|
||||
self::$init_options['admin_ui'] = $options['admin_ui'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the initialization options and returns whether the admin_ui should be initialized or not
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_initialize_admin_ui() {
|
||||
return isset( self::$init_options['admin_ui'] ) && true === self::$init_options['admin_ui'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize VideoPress features that should be initialized whenever VideoPress is present, even if the module is not active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function unconditional_initialization() {
|
||||
if ( self::should_include_utilities() ) {
|
||||
require_once __DIR__ . '/utility-functions.php';
|
||||
}
|
||||
|
||||
// Set up package version hook.
|
||||
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
|
||||
|
||||
Module_Control::init();
|
||||
|
||||
/*
|
||||
* The WPCOM REST API v2 endpoints only register routes/fields on REST
|
||||
* init, so defer constructing them (and autoloading their classes) until
|
||||
* a REST request is actually served. Registered on both REST init hooks
|
||||
* so the routes remain available in every context they were before, and
|
||||
* guarded so the endpoints are instantiated only once per request.
|
||||
*/
|
||||
$register_rest_api_v2_endpoints = static function () {
|
||||
static $registered = false;
|
||||
if ( $registered ) {
|
||||
return;
|
||||
}
|
||||
$registered = true;
|
||||
new WPCOM_REST_API_V2_Endpoint_VideoPress();
|
||||
new WPCOM_REST_API_V2_Attachment_VideoPress_Field();
|
||||
new WPCOM_REST_API_V2_Attachment_VideoPress_Data();
|
||||
};
|
||||
add_action( 'rest_api_init', $register_rest_api_v2_endpoints, 0 );
|
||||
add_action( 'restapi_theme_init', $register_rest_api_v2_endpoints, 0 );
|
||||
|
||||
if ( is_admin() ) {
|
||||
AJAX::init();
|
||||
} else {
|
||||
require_once __DIR__ . '/class-block-replacement.php';
|
||||
Block_Replacement::init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This avoids conflicts when running VideoPress plugin with older versions of the Jetpack plugin
|
||||
*
|
||||
* On version 11.3-a.7 utility functions include were removed from the plugin and it is safe to include it from the package
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private static function should_include_utilities() {
|
||||
if ( ! class_exists( 'Jetpack' ) || ! defined( 'JETPACK__VERSION' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return version_compare( JETPACK__VERSION, '11.3-a.7', '>=' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize VideoPress features that should be initialized only when the module is active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function active_initialization() {
|
||||
Attachment_Handler::init();
|
||||
Jwt_Token_Bridge::init();
|
||||
Initial_State::init();
|
||||
XMLRPC::init();
|
||||
Block_Editor_Content::init();
|
||||
|
||||
/*
|
||||
* These endpoints only add their routes on REST init, so defer calling
|
||||
* init() (and autoloading the endpoint classes) until a REST request is
|
||||
* served. Priority 0 ensures the routes still register before the
|
||||
* default-priority rest_api_init handlers run. Class-name strings are
|
||||
* used so the classes are not autoloaded on non-REST requests.
|
||||
*/
|
||||
foreach (
|
||||
array(
|
||||
Uploader_Rest_Endpoints::class,
|
||||
Rest_Controller::class,
|
||||
VideoPress_Rest_Api_V1_Stats::class,
|
||||
VideoPress_Rest_Api_V1_Site::class,
|
||||
VideoPress_Rest_Api_V1_Settings::class,
|
||||
VideoPress_Rest_Api_V1_Features::class,
|
||||
) as $rest_endpoint
|
||||
) {
|
||||
add_action( 'rest_api_init', array( $rest_endpoint, 'init' ), 0 );
|
||||
}
|
||||
self::register_oembed_providers();
|
||||
|
||||
// Enqueuethe VideoPress Iframe API script in the front-end.
|
||||
add_filter( 'embed_oembed_html', array( __CLASS__, 'enqueue_videopress_iframe_api_script' ), 10, 4 );
|
||||
|
||||
if ( self::should_initialize_admin_ui() ) {
|
||||
Admin_UI::init();
|
||||
}
|
||||
|
||||
Divi::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly register VideoPress oembed provider for patterns not supported by core
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_oembed_providers() {
|
||||
$host = rawurlencode( home_url() );
|
||||
// videopress.com/v is already registered in core.
|
||||
// By explicitly declaring the provider here, we can speed things up by not relying on oEmbed discovery.
|
||||
wp_oembed_add_provider( '#^https?://video.wordpress.com/v/.*#', 'https://public-api.wordpress.com/oembed/?for=' . $host, true );
|
||||
// This is needed as it's not supported in oEmbed discovery
|
||||
wp_oembed_add_provider( '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|i', 'https://public-api.wordpress.com/oembed/?for=' . $host, true ); // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
|
||||
|
||||
add_filter( 'embed_oembed_html', array( __CLASS__, 'video_enqueue_bridge_when_oembed_present' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues VideoPress token bridge when a VideoPress oembed is present on the current page.
|
||||
*
|
||||
* @param string|false $cache The cached HTML result, stored in post meta.
|
||||
* @param string $url The attempted embed URL.
|
||||
* @param array $attr An array of shortcode attributes.
|
||||
* @param int $post_ID Post ID.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public static function video_enqueue_bridge_when_oembed_present( $cache, $url, $attr, $post_ID = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( Utils::is_videopress_url( $url ) ) {
|
||||
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all VideoPress blocks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_videopress_blocks() {
|
||||
// Register VideoPress Video block.
|
||||
self::register_videopress_video_block();
|
||||
}
|
||||
|
||||
/**
|
||||
* VideoPress video block render method
|
||||
*
|
||||
* @global \WP_Embed $wp_embed WordPress embed handler.
|
||||
*
|
||||
* @param array $block_attributes Block attributes.
|
||||
* @param string $content Current block markup.
|
||||
* @param \WP_Block $block Current block.
|
||||
*
|
||||
* @return string Block markup.
|
||||
*/
|
||||
public static function render_videopress_video_block( $block_attributes, $content, $block ) {
|
||||
global $wp_embed;
|
||||
|
||||
// CSS classes.
|
||||
$align = $block_attributes['align'] ?? null;
|
||||
$align_class = $align ? ' align' . $align : '';
|
||||
$custom_class = isset( $block_attributes['className'] ) ? ' ' . $block_attributes['className'] : '';
|
||||
$classes = 'wp-block-jetpack-videopress jetpack-videopress-player' . $custom_class . $align_class;
|
||||
|
||||
// Inline style.
|
||||
$style = '';
|
||||
$max_width = $block_attributes['maxWidth'] ?? null;
|
||||
|
||||
if ( $max_width && $max_width !== '100%' ) {
|
||||
$style = sprintf( 'max-width: %s;', $max_width );
|
||||
$classes .= ' wp-block-jetpack-videopress--has-max-width';
|
||||
}
|
||||
|
||||
/*
|
||||
* <figcaption /> element
|
||||
* Caption is stored into the block attributes,
|
||||
* but also it was stored into the <figcaption /> element,
|
||||
* meaning that it could be stored in two different places.
|
||||
*/
|
||||
$figcaption = '';
|
||||
|
||||
// Caption from block attributes.
|
||||
$caption = $block_attributes['caption'] ?? null;
|
||||
|
||||
/*
|
||||
* If the caption is not stored into the block attributes,
|
||||
* try to get it from the <figcaption /> element.
|
||||
*/
|
||||
if ( $caption === null ) {
|
||||
preg_match( '/<figcaption>(.*?)<\/figcaption>/', $content, $matches );
|
||||
$caption = $matches[1] ?? null;
|
||||
}
|
||||
|
||||
// If we have a caption, create the <figcaption /> element.
|
||||
if ( $caption !== null ) {
|
||||
$figcaption = sprintf( '<figcaption>%s</figcaption>', wp_kses_post( $caption ) );
|
||||
}
|
||||
|
||||
// Custom anchor from block content.
|
||||
$id_attribute = '';
|
||||
|
||||
// Try to get the custom anchor from the block attributes.
|
||||
if ( isset( $block_attributes['anchor'] ) && $block_attributes['anchor'] ) {
|
||||
$id_attribute = sprintf( 'id="%s"', esc_attr( $block_attributes['anchor'] ) );
|
||||
} elseif ( preg_match( '/<figure[^>]*id="([^"]+)"/', $content, $matches ) ) {
|
||||
// Otherwise, try to get the custom anchor from the <figure /> element.
|
||||
$id_attribute = sprintf( 'id="%s"', esc_attr( $matches[1] ) );
|
||||
}
|
||||
|
||||
// Preview On Hover data.
|
||||
$is_poh_enabled =
|
||||
isset( $block_attributes['posterData']['previewOnHover'] ) &&
|
||||
$block_attributes['posterData']['previewOnHover'];
|
||||
|
||||
$autoplay = $block_attributes['autoplay'] ?? false;
|
||||
$controls = $block_attributes['controls'] ?? false;
|
||||
$poster = $block_attributes['posterData']['url'] ?? null;
|
||||
|
||||
$preview_on_hover = '';
|
||||
|
||||
if ( $is_poh_enabled ) {
|
||||
$preview_on_hover = array(
|
||||
'previewAtTime' => $block_attributes['posterData']['previewAtTime'],
|
||||
'previewLoopDuration' => $block_attributes['posterData']['previewLoopDuration'],
|
||||
'autoplay' => $autoplay,
|
||||
'showControls' => $controls,
|
||||
);
|
||||
|
||||
// Create inline style in case video has a custom poster.
|
||||
$inline_style = '';
|
||||
if ( $poster ) {
|
||||
$inline_style = sprintf(
|
||||
'style="background-image: url(%s); background-size: cover; background-position: center center;"',
|
||||
esc_attr( $poster )
|
||||
);
|
||||
}
|
||||
|
||||
// Expose the preview on hover data to the client.
|
||||
$preview_on_hover = sprintf(
|
||||
'<div class="jetpack-videopress-player__overlay" %s></div><script type="application/json">%s</script>',
|
||||
$inline_style,
|
||||
wp_json_encode( $preview_on_hover, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
|
||||
);
|
||||
|
||||
// Set `autoplay` and `muted` attributes to the video element.
|
||||
$block_attributes['autoplay'] = true;
|
||||
$block_attributes['muted'] = true;
|
||||
}
|
||||
|
||||
$figure_template = '
|
||||
<figure class="%1$s" style="%2$s" %3$s>
|
||||
%4$s
|
||||
%5$s
|
||||
%6$s
|
||||
</figure>
|
||||
';
|
||||
|
||||
// VideoPress URL.
|
||||
$guid = $block_attributes['guid'] ?? null;
|
||||
$videopress_url = Utils::get_video_press_url( $guid, $block_attributes );
|
||||
|
||||
$video_wrapper = '';
|
||||
$video_wrapper_classes = 'jetpack-videopress-player__wrapper';
|
||||
|
||||
if ( $videopress_url ) {
|
||||
$videopress_url = wp_kses_post( $videopress_url );
|
||||
|
||||
/*
|
||||
* Provide a fallback iframe for when the oEmbed endpoint fails, e.g.
|
||||
* when the VideoPress backend isn't ready for a freshly uploaded video.
|
||||
* This prevents the published page from showing a bare link.
|
||||
*/
|
||||
$fallback = function ( $output, $url ) use ( $videopress_url ) {
|
||||
if ( $url !== html_entity_decode( $videopress_url, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<iframe title="%1$s" aria-label="%1$s" src="%2$s" width="640" height="360" allowfullscreen data-resize-to-parent="true" allow="clipboard-write"></iframe>',
|
||||
esc_attr__( 'VideoPress Video Player', 'jetpack-videopress-pkg' ),
|
||||
esc_url( preg_replace( '#/v/#', '/embed/', $url, 1 ) )
|
||||
);
|
||||
};
|
||||
|
||||
add_filter( 'embed_maybe_make_link', $fallback, 10, 2 );
|
||||
$oembed_html = apply_filters( 'video_embed_html', $wp_embed->shortcode( array(), $videopress_url ) );
|
||||
remove_filter( 'embed_maybe_make_link', $fallback );
|
||||
|
||||
$video_wrapper = sprintf(
|
||||
'<div class="%s">%s %s</div>',
|
||||
$video_wrapper_classes,
|
||||
$preview_on_hover,
|
||||
$oembed_html
|
||||
);
|
||||
|
||||
/*
|
||||
* Self-heal failed oEmbed cache for VideoPress URLs.
|
||||
*
|
||||
* When the VideoPress backend isn't ready for a freshly uploaded video,
|
||||
* WordPress caches '{{unknown}}' in post meta with a TTL that is too long
|
||||
* for this use case. Clear recent failures so the next page render retries
|
||||
* oEmbed discovery, keeping the fallback iframe above temporary.
|
||||
*/
|
||||
$post_id = $block->context['postId'] ?? get_the_ID();
|
||||
|
||||
if ( $post_id ) {
|
||||
$key_suffix = md5( $videopress_url . serialize( wp_embed_defaults( $videopress_url ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Matching WP_Embed cache key format.
|
||||
$oembed_value = get_post_meta( $post_id, '_oembed_' . $key_suffix, true );
|
||||
$oembed_time = (int) get_post_meta( $post_id, '_oembed_time_' . $key_suffix, true );
|
||||
|
||||
/*
|
||||
* Only clear the '{{unknown}}' cache entry when it is recent, to avoid
|
||||
* disabling WordPress's oEmbed backoff for persistent provider failures.
|
||||
*/
|
||||
if (
|
||||
'{{unknown}}' === $oembed_value
|
||||
&& ( ! $oembed_time || ( time() - $oembed_time ) < MINUTE_IN_SECONDS )
|
||||
) {
|
||||
delete_post_meta( $post_id, '_oembed_' . $key_suffix );
|
||||
delete_post_meta( $post_id, '_oembed_time_' . $key_suffix );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get premium content from block context.
|
||||
$premium_block_plan_id = isset( $block->context['premium-content/planId'] ) ? intval( $block->context['premium-content/planId'] ) : 0;
|
||||
$is_premium_content_child = isset( $block->context['isPremiumContentChild'] ) ? (bool) $block->context['isPremiumContentChild'] : false;
|
||||
$maybe_premium_script = '';
|
||||
if ( $is_premium_content_child ) {
|
||||
Access_Control::instance()->set_guid_subscription( $guid, $premium_block_plan_id );
|
||||
$escaped_guid = wp_json_encode( $guid, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
|
||||
$script_content = "if ( ! window.__guidsToPlanIds ) { window.__guidsToPlanIds = {}; }; window.__guidsToPlanIds[$escaped_guid] = $premium_block_plan_id;";
|
||||
$maybe_premium_script = '<script>' . $script_content . '</script>';
|
||||
}
|
||||
|
||||
// $id_attribute, $video_wrapper, $figcaption properly escaped earlier in the code.
|
||||
return sprintf(
|
||||
$figure_template,
|
||||
esc_attr( $classes ),
|
||||
esc_attr( $style ),
|
||||
$id_attribute,
|
||||
$video_wrapper,
|
||||
$figcaption,
|
||||
$maybe_premium_script
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the VideoPress block editor block,
|
||||
* AKA "VideoPress Block v6".
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_videopress_video_block() {
|
||||
/*
|
||||
* If only Jetpack is active, and if the VideoPress module is not active,
|
||||
* we can register the block just to display a placeholder to turn on the module.
|
||||
* That invitation is only useful for admins though.
|
||||
*/
|
||||
if (
|
||||
Status::is_jetpack_plugin_without_videopress_module_active()
|
||||
&& ! Status::is_standalone_plugin_active()
|
||||
&& ! current_user_can( 'jetpack_activate_modules' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$videopress_video_metadata_file = __DIR__ . '/../build/block-editor/blocks/video/block.json';
|
||||
$videopress_video_metadata_file_exists = file_exists( $videopress_video_metadata_file );
|
||||
if ( ! $videopress_video_metadata_file_exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$videopress_video_metadata = json_decode(
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
file_get_contents( $videopress_video_metadata_file )
|
||||
);
|
||||
|
||||
// Pick the block name straight from the block metadata .json file.
|
||||
$videopress_video_block_name = $videopress_video_metadata->name;
|
||||
|
||||
// Is the block already registered?
|
||||
$is_block_registered = \WP_Block_Type_Registry::get_instance()->is_registered( $videopress_video_block_name );
|
||||
|
||||
// Do not register if the block is already registered.
|
||||
if ( $is_block_registered ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registration = register_block_type(
|
||||
$videopress_video_metadata_file,
|
||||
array(
|
||||
'render_callback' => array( __CLASS__, 'render_videopress_video_block' ),
|
||||
'render_email_callback' => array( Video_Block_Email_Renderer::class, 'render' ),
|
||||
'uses_context' => array( 'premium-content/planId', 'isPremiumContentChild', 'selectedPlanId' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Do not enqueue scripts if the block could not be registered.
|
||||
if ( empty( $registration ) || empty( $registration->editor_script_handles ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extensions use Connection_Initial_State::render_script with script handle as parameter.
|
||||
if ( is_array( $registration->editor_script_handles ) ) {
|
||||
$script_handle = $registration->editor_script_handles[0];
|
||||
} else {
|
||||
$script_handle = $registration->editor_script_handles;
|
||||
}
|
||||
|
||||
// Register and enqueue scripts used by the VideoPress video block.
|
||||
Block_Editor_Extensions::init( $script_handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the VideoPress Iframe API script
|
||||
* when the URL of oEmbed HTML is a VideoPress URL.
|
||||
*
|
||||
* @param string|false $cache The cached HTML result, stored in post meta.
|
||||
* @param string $url The attempted embed URL.
|
||||
* @param array $attr An array of shortcode attributes.
|
||||
* @param int $post_ID Post ID.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public static function enqueue_videopress_iframe_api_script( $cache, $url, $attr, $post_ID ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( Utils::is_videopress_url( $url ) ) {
|
||||
// Enqueue the VideoPress IFrame API in the front-end.
|
||||
wp_enqueue_script(
|
||||
self::JETPACK_VIDEOPRESS_IFRAME_API_HANDLER,
|
||||
'https://s0.wp.com/wp-content/plugins/video/assets/js/videojs/videopress-iframe-api.js',
|
||||
array(),
|
||||
gmdate( 'YW' ),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Jwt_Token_Bridge
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* VideoPress Jwt_Token_Bridge class.
|
||||
*/
|
||||
class Jwt_Token_Bridge {
|
||||
|
||||
/**
|
||||
* The handle used to enqueue the script
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCRIPT_HANDLE = 'media-video-jwt-bridge';
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
* This method should be called only once by the Initializer class. Do not call this method again.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! Status::is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Expose the VideoPress token to the Block Editor context.
|
||||
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_jwt_token_bridge' ), 1 );
|
||||
|
||||
// Expose the VideoPress token to the WPAdmin context.
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_jwt_token_bridge' ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bridge script URL depending on the environment we are in
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_bridge_url() {
|
||||
return plugins_url( '../build/lib/token-bridge.js', __FILE__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the jwt bridge script.
|
||||
*/
|
||||
public static function enqueue_jwt_token_bridge() {
|
||||
global $post;
|
||||
$post_id = isset( $post->ID ) ? absint( $post->ID ) : 0;
|
||||
|
||||
$bridge_url = self::get_bridge_url();
|
||||
|
||||
self::enqueue_script();
|
||||
|
||||
wp_localize_script(
|
||||
self::SCRIPT_HANDLE,
|
||||
'videopressAjax',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'bridgeUrl' => $bridge_url,
|
||||
'post_id' => $post_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues only the JS script
|
||||
*
|
||||
* @param string $handle The script handle to identify the script.
|
||||
* @return bool True if the script was successfully localized, false otherwise.
|
||||
*/
|
||||
public static function enqueue_script( $handle = self::SCRIPT_HANDLE ) {
|
||||
return wp_enqueue_script(
|
||||
$handle,
|
||||
self::get_bridge_url(),
|
||||
array(),
|
||||
Package_Version::PACKAGE_VERSION,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack VideoPress: Module_Control class
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* To handle VideoPress module statuses
|
||||
*/
|
||||
class Module_Control {
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*
|
||||
* This method should onlybe called once by the Initializer class. Do not call this method again.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'jetpack_get_available_standalone_modules', array( __CLASS__, 'add_videopress_to_array' ), 10, 1 );
|
||||
if ( Status::is_standalone_plugin_active() ) {
|
||||
// If the stand-alone plugin is active, videopress module will always be considered active
|
||||
add_filter( 'jetpack_active_modules', array( __CLASS__, 'add_videopress_to_array' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds videopress to the list of available/active modules
|
||||
*
|
||||
* @param array $modules Array with modules slugs.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_videopress_to_array( $modules ) {
|
||||
return array_merge( array( 'videopress' ), $modules );
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Options
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Current_Plan;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* VideoPress Options class.
|
||||
*/
|
||||
class Options {
|
||||
|
||||
/**
|
||||
* Option name.
|
||||
*
|
||||
* @var string $option_name The 'videopress' option name
|
||||
*/
|
||||
public static $option_name = 'videopress';
|
||||
|
||||
/**
|
||||
* VideoPress Options.
|
||||
*
|
||||
* @var array $options An array of associated VideoPress options (default empty)
|
||||
*/
|
||||
protected static $options = array();
|
||||
|
||||
/**
|
||||
* Get VideoPress options
|
||||
*
|
||||
* @return array An array of VideoPress options.
|
||||
*/
|
||||
public static function get_options() {
|
||||
// Make sure we only get options from the database and services once per connection.
|
||||
if ( array() !== self::$options ) {
|
||||
return self::$options;
|
||||
}
|
||||
|
||||
$defaults = array(
|
||||
'meta' => array(
|
||||
'max_upload_size' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
self::$options = Jetpack_Options::get_option( self::$option_name, array() );
|
||||
self::$options = array_merge( $defaults, self::$options );
|
||||
|
||||
// Make sure that the shadow blog id never comes from the options, but instead uses the
|
||||
// associated shadow blog id, if videopress is enabled.
|
||||
self::$options['shadow_blog_id'] = 0;
|
||||
|
||||
// Use the Jetpack ID for the shadow blog ID if we have a plan that supports VideoPress.
|
||||
if ( Current_Plan::supports( 'videopress' ) ) {
|
||||
self::$options['shadow_blog_id'] = Jetpack_Options::get_option( 'id' );
|
||||
}
|
||||
|
||||
return self::$options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update VideoPress options
|
||||
*
|
||||
* @param mixed $options VideoPress options.
|
||||
*/
|
||||
public static function update_options( $options ) {
|
||||
Jetpack_Options::update_option( self::$option_name, $options );
|
||||
|
||||
self::$options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the VideoPress module is deactivated.
|
||||
*/
|
||||
public static function delete_options() {
|
||||
Jetpack_Options::delete_option( self::$option_name );
|
||||
|
||||
self::$options = array();
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*/
|
||||
class Package_Version {
|
||||
const PACKAGE_VERSION = '0.38.2';
|
||||
|
||||
const PACKAGE_SLUG = 'videopress';
|
||||
|
||||
/**
|
||||
* Adds the package slug and version to the package version tracker's data.
|
||||
*
|
||||
* @param array $package_versions The package version array.
|
||||
*
|
||||
* @return array The packge version array.
|
||||
*/
|
||||
public static function send_package_version_to_tracker( $package_versions ) {
|
||||
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
|
||||
return $package_versions;
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/**
|
||||
* The Plan class.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* The Plan class.
|
||||
*/
|
||||
class Plan {
|
||||
/**
|
||||
* The meta name used to store the cache date
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_DATE_META_NAME = 'videopress-cache-date';
|
||||
|
||||
/**
|
||||
* Valid pediord for the cache: One week.
|
||||
*/
|
||||
const CACHE_VALIDITY_PERIOD = 7 * DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* The meta name used to store the cache
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_META_NAME = 'videopress-cache';
|
||||
|
||||
/**
|
||||
* Checks if the cache is old, meaning we need to fetch new data from WPCOM
|
||||
*/
|
||||
private static function is_cache_old() {
|
||||
if ( empty( self::get_product_from_cache() ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cache_date = get_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, true );
|
||||
return time() - (int) $cache_date > ( self::CACHE_VALIDITY_PERIOD );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product list from the user cache
|
||||
*/
|
||||
private static function get_product_from_cache() {
|
||||
return get_user_meta( get_current_user_id(), self::CACHE_META_NAME, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product() {
|
||||
$request_url = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
|
||||
$wpcom_request = wp_remote_get( esc_url_raw( $request_url ) );
|
||||
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
|
||||
|
||||
if ( 200 === $response_code ) {
|
||||
$products = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
|
||||
if ( ! isset( $products->jetpack_videopress ) || ! isset( $products->jetpack_videopress_monthly ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Pick the desired product...
|
||||
$product = $products->jetpack_videopress;
|
||||
|
||||
// ... and store it into the cache.
|
||||
update_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, time() );
|
||||
update_user_meta( get_current_user_id(), self::CACHE_META_NAME, $product );
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'failed_to_fetch_data',
|
||||
esc_html__( 'Unable to fetch the requested data.', 'jetpack-videopress-pkg' ),
|
||||
array(
|
||||
'status' => $response_code,
|
||||
'request' => $wpcom_request,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the pricing array with the discount information.
|
||||
*
|
||||
* @param object $product - The product object.
|
||||
* @return int|false Discount percentage.
|
||||
*/
|
||||
public static function get_coupon_discount( $product ) {
|
||||
// Check whether the product has a coupon.
|
||||
if ( ! isset( $product->sale_coupon ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product_id = $product->product_id;
|
||||
$coupon = $product->sale_coupon;
|
||||
|
||||
// Check product is covered by the coupon.
|
||||
if ( ! in_array( $product_id, $coupon->product_ids, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whether it is still valid.
|
||||
$coupon_start_date = strtotime( $coupon->start_date );
|
||||
$coupon_expires = strtotime( $coupon->expires );
|
||||
if ( $coupon_start_date > time() || $coupon_expires < time() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $coupon->discount ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval( $coupon->discount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return details about the VideoPress product price
|
||||
*
|
||||
* @return array Produce price details
|
||||
*/
|
||||
public static function get_product_price() {
|
||||
$request_url = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
|
||||
$wpcom_request = wp_remote_get( esc_url_raw( $request_url ) );
|
||||
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
|
||||
|
||||
if ( 200 === $response_code ) {
|
||||
$products = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
|
||||
|
||||
$products_list = array();
|
||||
|
||||
if ( isset( $products->jetpack_videopress ) ) {
|
||||
$videopress_yearly = $products->jetpack_videopress;
|
||||
// get_coupon_discount
|
||||
$products_list['yearly'] = array(
|
||||
'name' => $videopress_yearly->product_name,
|
||||
'slug' => $videopress_yearly->product_slug,
|
||||
'price' => $videopress_yearly->cost,
|
||||
'priceByMonth' => round( $videopress_yearly->cost / 12, 2 ),
|
||||
'currency' => $videopress_yearly->currency_code,
|
||||
);
|
||||
|
||||
$discount = self::get_coupon_discount( $videopress_yearly );
|
||||
|
||||
if ( $discount ) {
|
||||
$products_list['yearly']['discount'] = $discount;
|
||||
$products_list['yearly']['salePrice'] = round( $videopress_yearly->cost * ( 1 - $discount / 100 ), 2 );
|
||||
$products_list['yearly']['salePriceByMonth'] = round( ( $videopress_yearly->cost * ( 1 - $discount / 100 ) / 12 ), 2 );
|
||||
} else {
|
||||
$products_list['yearly']['salePrice'] = $videopress_yearly->cost;
|
||||
$products_list['yearly']['salePriceByMonth'] = round( $videopress_yearly->cost / 12, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $products->jetpack_videopress_monthly ) ) {
|
||||
$videopress_monthly = $products->jetpack_videopress_monthly;
|
||||
|
||||
$products_list['monthly'] = array(
|
||||
'name' => $videopress_monthly->product_name,
|
||||
'slug' => $videopress_monthly->product_slug,
|
||||
'price' => $videopress_monthly->cost,
|
||||
'currency' => $videopress_monthly->currency_code,
|
||||
);
|
||||
}
|
||||
|
||||
return $products_list;
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'failed_to_fetch_data',
|
||||
esc_html__( 'Unable to fetch the requested data.', 'jetpack-videopress-pkg' ),
|
||||
array(
|
||||
'status' => $response_code,
|
||||
'request' => $wpcom_request,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/**
|
||||
* The VideoPress REST Controller.
|
||||
*
|
||||
* Registers the `/jetpack/v4/videopress/*` routes backing the
|
||||
* modernized wp-build dashboard. Currently exposes one route — a
|
||||
* user-signed proxy to the WPCOM `sites/{id}/stats/video-plays`
|
||||
* endpoint — needed by the Overview screen's KPI / trends / top-N
|
||||
* cards.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* REST routes for the modernized VideoPress admin UI.
|
||||
*/
|
||||
class Rest_Controller {
|
||||
|
||||
/**
|
||||
* REST namespace used by this package's modernization routes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REST_NAMESPACE = 'jetpack/v4/videopress';
|
||||
|
||||
/**
|
||||
* Hook the route registration on `rest_api_init`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the VideoPress REST routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/stats/video-plays',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_stats_video_plays' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_callback' ),
|
||||
'args' => self::stats_video_plays_args(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params accepted by the video-plays proxy. Forwarded verbatim
|
||||
* to WPCOM after permission and shape validation; `complete_stats` is
|
||||
* always forced to `true` (the only mode the Overview cares about)
|
||||
* and is therefore not exposed as an incoming param.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function stats_video_plays_args() {
|
||||
return array(
|
||||
'period' => array(
|
||||
'description' => __( 'Period unit: day, week, month, or year.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
||||
),
|
||||
'num' => array(
|
||||
'description' => __( 'Number of periods to include.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
'maximum' => 365,
|
||||
),
|
||||
'date' => array(
|
||||
'description' => __( 'Most recent day to include in results (YYYY-MM-DD).', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date',
|
||||
),
|
||||
'start_date' => array(
|
||||
'description' => __( 'Starting date for range queries (YYYY-MM-DD).', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback. Admin-gated. The upstream call is blog-signed,
|
||||
* matching the existing `Stats::fetch_video_plays` path; no user-level
|
||||
* WPCOM connection is required.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy the video-plays stats endpoint.
|
||||
*
|
||||
* Forwards the whitelisted query params to WPCOM (REST v1.1, blog-signed
|
||||
* — matching the existing `Stats::fetch_video_plays` path) and forces
|
||||
* `complete_stats=true`. In complete-stats mode, each day entry carries
|
||||
* `total.views`, `total.impressions`, and `total.watch_time` (in hours)
|
||||
* plus a per-video `data[]` array whose entries have `post_id`, `title`,
|
||||
* `views`, `impressions`, `watch_time` (hours), and `retention_rate`.
|
||||
* The `plays` field is NOT returned in complete-stats mode.
|
||||
*
|
||||
* @param WP_REST_Request $request Incoming request.
|
||||
* @return mixed Decoded JSON response from WPCOM, or WP_Error on failure.
|
||||
*/
|
||||
public static function get_stats_video_plays( WP_REST_Request $request ) {
|
||||
$blog_id = (int) Jetpack_Options::get_option( 'id' );
|
||||
if ( ! $blog_id ) {
|
||||
return new WP_Error(
|
||||
'videopress_stats_not_connected',
|
||||
esc_html__( 'This site is not connected to WordPress.com.', 'jetpack-videopress-pkg' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$params = array( 'complete_stats' => 'true' );
|
||||
foreach ( array_keys( self::stats_video_plays_args() ) as $key ) {
|
||||
$value = $request->get_param( $key );
|
||||
if ( $value !== null && $value !== '' ) {
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$path = sprintf(
|
||||
'sites/%d/stats/video-plays?%s',
|
||||
$blog_id,
|
||||
http_build_query( $params )
|
||||
);
|
||||
$response = Client::wpcom_json_api_request_as_blog( $path );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'videopress_stats_request_failed',
|
||||
$response->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code( $response );
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( 200 !== $status ) {
|
||||
$message = is_array( $body ) && isset( $body['message'] )
|
||||
? (string) $body['message']
|
||||
: esc_html__( 'Unable to fetch VideoPress stats.', 'jetpack-videopress-pkg' );
|
||||
return new WP_Error(
|
||||
'videopress_stats_request_failed',
|
||||
$message,
|
||||
array( 'status' => $status ? $status : 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $body );
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Provides site data sourced from WPCOM
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Provides site data sourced from WPCOM
|
||||
*/
|
||||
class Site {
|
||||
|
||||
/**
|
||||
* Returns all the data provided by WPCOM for the site.
|
||||
*
|
||||
* @return int|WP_Error the total of plays for today, or WP_Error on failure.
|
||||
*/
|
||||
public static function get_site_info() {
|
||||
$error = new WP_Error(
|
||||
'videopress_site_error',
|
||||
__( 'Could not fetch site information from the service', 'jetpack-videopress-pkg' )
|
||||
);
|
||||
|
||||
$request_path = sprintf( 'sites/%d?force=wpcom', Data::get_blog_id() );
|
||||
$response = Client::wpcom_json_api_request_as_blog( $request_path, '1.1', array(), null, 'rest' );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( 200 !== $response_code ) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
return json_decode( $body, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the purchases provided by WPCOM for the site.
|
||||
*
|
||||
* @return array the list of purchases, or an empty list on failure.
|
||||
*/
|
||||
public static function get_purchases() {
|
||||
$request_path = sprintf( 'sites/%1$d/purchases?locale=%2$s', Data::get_blog_id(), get_user_locale() );
|
||||
$response = Client::wpcom_json_api_request_as_blog( $request_path, '1.1', array(), null, 'rest' );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( 200 !== $response_code ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
return json_decode( $body, true );
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* Provides data stats about videos inside VideoPress
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Provides data stats about videos inside VideoPress
|
||||
*/
|
||||
class Stats {
|
||||
/**
|
||||
* Hit WPCOM video-plays stats endpoint.
|
||||
*
|
||||
* @param array $args Request args.
|
||||
* @return array|WP_Error WP HTTP response on success
|
||||
*/
|
||||
public static function fetch_video_plays( $args = array() ) {
|
||||
$blog_id = VideoPressToken::blog_id();
|
||||
|
||||
$endpoint = sprintf(
|
||||
'sites/%d/stats/video-plays?check_stats_module=false',
|
||||
$blog_id
|
||||
);
|
||||
|
||||
if ( is_array( $args ) && ! empty( $args ) ) {
|
||||
$endpoint .= '&' . http_build_query( $args );
|
||||
}
|
||||
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = $result['http_response'];
|
||||
$response_code = $response->get_status();
|
||||
$response_body = json_decode( $response->get_data(), true );
|
||||
|
||||
if ( 200 !== $response_code ) {
|
||||
return new WP_Error(
|
||||
'videopress_stats_error',
|
||||
$response_body
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'code' => $response_code,
|
||||
'data' => $response_body,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the counter of today's plays for all videos.
|
||||
*
|
||||
* @return int|WP_Error the total of plays for today, or WP_Error on failure.
|
||||
*/
|
||||
public static function get_today_plays() {
|
||||
$response = self::fetch_video_plays();
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = $response['data'];
|
||||
|
||||
if ( ! $data || ! isset( $data['days'] ) || ! is_countable( $data['days'] ) || count( $data['days'] ) === 0 ) {
|
||||
return new WP_Error(
|
||||
'videopress_stats_error',
|
||||
__( 'Could not find any stats from the service', 'jetpack-videopress-pkg' )
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* The only result here is today's stats
|
||||
*/
|
||||
$today_stats = array_pop( $data['days'] );
|
||||
|
||||
return $today_stats['total_plays'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the featured stats for VideoPress.
|
||||
*
|
||||
* @param int $period_count (optional) The number of days to consider.
|
||||
* @param string $period (optional) The period to consider.
|
||||
*
|
||||
* @return array|WP_Error a list of stats, or WP_Error on failure.
|
||||
*/
|
||||
public static function get_featured_stats( $period_count = 14, $period = 'day' ) {
|
||||
$response = self::fetch_video_plays(
|
||||
array(
|
||||
'period' => $period,
|
||||
'num' => $period_count,
|
||||
'complete_stats' => true,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = $response['data'];
|
||||
|
||||
if ( ! $data ) {
|
||||
return new WP_Error(
|
||||
'videopress_stats_error',
|
||||
__( 'Could not find any stats from the service', 'jetpack-videopress-pkg' )
|
||||
);
|
||||
}
|
||||
|
||||
// Get only the list of dates
|
||||
$dates = $data['days'];
|
||||
|
||||
// Organize the data into the planned stats
|
||||
return self::prepare_featured_stats( $dates, $period_count, $period );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the featured stats for VideoPress.
|
||||
*
|
||||
* @param array $dates The list of dates returned by the API.
|
||||
* @param int $period_count The total number of days to consider.
|
||||
* @param string $period The period to consider.
|
||||
* @return array a list of stats.
|
||||
*/
|
||||
public static function prepare_featured_stats( $dates, $period_count, $period = 'day' ) {
|
||||
/**
|
||||
* Ensure the sorting of the dates, recent ones first.
|
||||
* This way, the first 7 positions are from the last 7 days,
|
||||
* and the next 7 positions are from the 7 days before it.
|
||||
*/
|
||||
krsort( $dates );
|
||||
$period_of_data = floor( $period_count / 2 );
|
||||
$period = $period === 'day' ? __( 'day', 'jetpack-videopress-pkg' ) : __( 'year', 'jetpack-videopress-pkg' );
|
||||
|
||||
// template for the response
|
||||
$featured_stats = array(
|
||||
// translators: %1$d is the number of units of time, %2$s is the period in which the units of time are measured ex. 'day' or 'year'.
|
||||
'label' => sprintf( _n( 'last %1$d %2$s', 'last %1$d %2$ss', (int) $period_of_data, 'jetpack-videopress-pkg' ), $period_of_data, $period ),
|
||||
'period' => $period,
|
||||
'data' => array(
|
||||
'views' => array(
|
||||
'current' => 0,
|
||||
'previous' => 0,
|
||||
),
|
||||
'impressions' => array(
|
||||
'current' => 0,
|
||||
'previous' => 0,
|
||||
),
|
||||
'watch_time' => array(
|
||||
'current' => 0,
|
||||
'previous' => 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Go through the dates to compute the stats
|
||||
$counter = 0;
|
||||
foreach ( $dates as $date_info ) {
|
||||
$date_totals = $date_info['total'];
|
||||
|
||||
if ( $counter < floor( $period_count / 2 ) ) {
|
||||
|
||||
// the first 7 elements are for the current period
|
||||
$featured_stats['data']['views']['current'] += $date_totals['views'];
|
||||
$featured_stats['data']['impressions']['current'] += $date_totals['impressions'];
|
||||
$featured_stats['data']['watch_time']['current'] += $date_totals['watch_time'];
|
||||
|
||||
} else {
|
||||
|
||||
// the next 7 elements are for the previous period
|
||||
$featured_stats['data']['views']['previous'] += $date_totals['views'];
|
||||
$featured_stats['data']['impressions']['previous'] += $date_totals['impressions'];
|
||||
$featured_stats['data']['watch_time']['previous'] += $date_totals['watch_time'];
|
||||
|
||||
}
|
||||
|
||||
++$counter;
|
||||
}
|
||||
|
||||
return $featured_stats;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* The class that provides information about VideoPress Status
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Modules;
|
||||
|
||||
/**
|
||||
* The class that provides information about VideoPress Status
|
||||
*/
|
||||
class Status {
|
||||
|
||||
/**
|
||||
* Returns whether VideoPress is active
|
||||
* either as a Jetpack module or as a stand alone plugin
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_active() {
|
||||
return self::is_jetpack_plugin_and_videopress_module_active() || self::is_standalone_plugin_active();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Jetpack plugin is active
|
||||
*/
|
||||
public static function is_jetpack_plugin_active() {
|
||||
return class_exists( 'Jetpack' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Jetpack plugin
|
||||
* and its VideoPress module are active.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_jetpack_plugin_and_videopress_module_active() {
|
||||
return class_exists( 'Jetpack' ) && ( new Modules() )->is_active( 'videopress' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Jetpack plugin is active
|
||||
* but the VideoPress module is not active.
|
||||
*
|
||||
* @since 0.28.2
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_jetpack_plugin_without_videopress_module_active() {
|
||||
return class_exists( 'Jetpack' ) && ! ( new Modules() )->is_active( 'videopress' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the VideoPress stand alone plugin is active
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_standalone_plugin_active() {
|
||||
return class_exists( 'Jetpack_VideoPress_Plugin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the registrant plugin is active
|
||||
* either as a Jetpack module (via Jetpack plugin)
|
||||
* or as a stand-alone plugin.
|
||||
*
|
||||
* @return boolean True if the register plugin is active.
|
||||
*/
|
||||
public static function is_registrant_plugin_active() {
|
||||
return self::is_jetpack_plugin_active() || self::is_standalone_plugin_active();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Uploader Exception
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* VideoPress Uploader Exception class
|
||||
*/
|
||||
class Upload_Exception extends \Exception {
|
||||
const ERROR_INVALID_ATTACHMENT_ID = 0;
|
||||
const ERROR_FILE_NOT_FOUND = 1;
|
||||
const ERROR_MIME_TYPE_NOT_SUPPORTED = 2;
|
||||
const ERROR_MALFORMED_FILE = 3;
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Uploader
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* VideoPress Uploader class
|
||||
*
|
||||
* Handles the upload from the Media Library to VideoPress servers
|
||||
*/
|
||||
class Uploader_Rest_Endpoints {
|
||||
|
||||
/**
|
||||
* Initializes the endpoints
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
$id_arg = array(
|
||||
'description' => __( 'The ID of the attachment you want to upload to VideoPress', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'validate_callback' => __CLASS__ . '::validate_attachment_id',
|
||||
);
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'upload/(?P<attachment_id>\d+)',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::check_status',
|
||||
'permission_callback' => __CLASS__ . '::permissions_callback',
|
||||
'args' => array(
|
||||
'attachment_id' => $id_arg,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::do_upload',
|
||||
'permission_callback' => __CLASS__ . '::permissions_callback',
|
||||
'args' => array(
|
||||
'attachment_id' => $id_arg,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether the user have permission to perform the upload
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
return current_user_can( 'upload_files' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the attachment ID argument
|
||||
*
|
||||
* @param integer|string $value The attachment ID passed as an argument to the endpoint.
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function validate_attachment_id( $value ) {
|
||||
return Uploader::is_valid_attachment_id( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint callback for the GET method. Checks the upload status
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function check_status( $request ) {
|
||||
$attachment_id = $request->get_param( 'attachment_id' );
|
||||
try {
|
||||
$uploader = new Uploader( $attachment_id );
|
||||
$status = $uploader->check_status();
|
||||
return rest_ensure_response( $status );
|
||||
} catch ( Upload_Exception $e ) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
$e->getMessage(),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint callback for the POST method. Uploads the video
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function do_upload( $request ) {
|
||||
$attachment_id = $request->get_param( 'attachment_id' );
|
||||
try {
|
||||
$uploader = new Uploader( $attachment_id );
|
||||
$status = $uploader->upload();
|
||||
return rest_ensure_response( $status );
|
||||
} catch ( Upload_Exception $e ) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
$e->getMessage(),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Uploader
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Jetpack_Options;
|
||||
use VideoPressUploader\File_Exception;
|
||||
use VideoPressUploader\Tus_Client;
|
||||
|
||||
/**
|
||||
* VideoPress Uploader class
|
||||
*
|
||||
* Handles the upload from the Media Library to VideoPress servers
|
||||
*/
|
||||
class Uploader {
|
||||
|
||||
/**
|
||||
* The key of the post meta that holds the ID of the attachment that holds the VideoPress video, in case this attachment was uploaded before.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const UPLOADED_KEY = '_videopress_uploaded_id';
|
||||
|
||||
/**
|
||||
* The chunk size of each upload step
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CHUNK_SIZE = 5000000;
|
||||
|
||||
/**
|
||||
* The Tus Client instance
|
||||
*
|
||||
* @var Tus_Client
|
||||
*/
|
||||
protected $client = null;
|
||||
|
||||
/**
|
||||
* The attachment ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $attachment_id;
|
||||
|
||||
/**
|
||||
* Checks whether this feature is supported by the server
|
||||
*
|
||||
* @param int $attachment_id The ID of the video attachment we want to upload to VideoPress.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_valid_attachment_id( $attachment_id ) {
|
||||
$file_path = get_attached_file( $attachment_id );
|
||||
if ( ! $file_path || ! is_readable( $file_path ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! str_starts_with( get_post_mime_type( $attachment_id ), 'video/' ) ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the object
|
||||
*
|
||||
* @throws Upload_Exception If attachment is invalid or server does not support it.
|
||||
* @param int $attachment_id The ID of the video attachment we want to upload to VideoPress.
|
||||
*/
|
||||
public function __construct( $attachment_id ) {
|
||||
$this->attachment_id = $attachment_id;
|
||||
if ( ! $this->get_file_path() ) {
|
||||
throw new Upload_Exception( __( 'Invalid attachment ID', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_INVALID_ATTACHMENT_ID );
|
||||
}
|
||||
if ( ! is_readable( $this->get_file_path() ) ) {
|
||||
throw new Upload_Exception( __( 'File not found', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_FILE_NOT_FOUND );
|
||||
}
|
||||
if ( ! $this->file_has_supported_mime_type() ) {
|
||||
throw new Upload_Exception( __( 'Mime type not supported', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_MIME_TYPE_NOT_SUPPORTED );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path of the video file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_path() {
|
||||
return get_attached_file( $this->attachment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mime type of the attachment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_mime_type() {
|
||||
return get_post_mime_type( $this->attachment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the video file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_name() {
|
||||
return basename( $this->get_file_path() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the video file
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_file_size() {
|
||||
return filesize( $this->get_file_path() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the mime type of the attachment is supported to be uploaded
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function file_has_supported_mime_type() {
|
||||
return str_starts_with( $this->get_file_mime_type(), 'video/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress upload token
|
||||
*
|
||||
* @throws Upload_Exception If it fails to fetch the token.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_upload_token() {
|
||||
return VideoPressToken::videopress_upload_jwt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a unique upload key for this attachment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_key() {
|
||||
return sprintf( 's-%d-v-%d', Jetpack_Options::get_option( 'id' ), $this->attachment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current attachment as uploaded and stores the ID of the VideoPress video attachment ID
|
||||
*
|
||||
* @param int $new_attachment_id The ID of the new attachment created to hold the VideoPress video.
|
||||
* @return void
|
||||
*/
|
||||
protected function mark_as_uploaded( $new_attachment_id ) {
|
||||
update_post_meta( $this->attachment_id, self::UPLOADED_KEY, $new_attachment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current attachment as not being uploaded before. Deletes the reference to the videopress attachment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function unmark_as_uploaded() {
|
||||
delete_post_meta( $this->attachment_id, self::UPLOADED_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this attachment was uploaded before
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_uploaded() {
|
||||
return ! empty( $this->get_uploaded_attachment_id() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of the VideoPress video attachment in case this attachment was uploaded before
|
||||
*
|
||||
* @return boolean|string False if value is absent. Post ID on success.
|
||||
*/
|
||||
public function get_uploaded_attachment_id() {
|
||||
return get_post_meta( $this->attachment_id, self::UPLOADED_KEY, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the instance of the Tus_Client
|
||||
*
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function get_client() {
|
||||
if ( $this->client !== null ) {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
$this->client = new Tus_Client( $this->get_key(), $this->get_upload_token(), Jetpack_Options::get_option( 'id' ) );
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a chunk of the file
|
||||
*
|
||||
* @return array With the status of the upload
|
||||
*/
|
||||
public function upload() {
|
||||
if ( $this->is_uploaded() ) {
|
||||
return $this->check_status();
|
||||
}
|
||||
try {
|
||||
$this->get_client()->file( $this->get_file_path(), $this->get_file_name() );
|
||||
|
||||
$bytes_uploaded = $this->get_client()->upload( self::CHUNK_SIZE );
|
||||
|
||||
if ( $bytes_uploaded === $this->get_file_size() ) {
|
||||
$this->mark_as_uploaded( $this->get_client()->get_uploaded_video_details()['media_id'] );
|
||||
return array(
|
||||
'status' => 'complete',
|
||||
'bytes_uploaded' => $bytes_uploaded,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'upload_key' => $this->get_key(),
|
||||
'uploaded_details' => $this->get_client()->get_uploaded_video_details(),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => 'uploading',
|
||||
'bytes_uploaded' => $bytes_uploaded,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'upload_key' => $this->get_key(),
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return array(
|
||||
'status' => 'error',
|
||||
'bytes_uploaded' => -1,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'upload_key' => $this->get_key(),
|
||||
'error' => $e->getCode() . ': ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of the upload of this attachment
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function check_status() {
|
||||
|
||||
if ( $this->is_uploaded() ) {
|
||||
$uploaded_attachment_id = $this->get_uploaded_attachment_id();
|
||||
$uploaded_video_guid = get_post_meta( $uploaded_attachment_id, 'videopress_guid', true );
|
||||
if ( $uploaded_video_guid ) {
|
||||
return array(
|
||||
'status' => 'uploaded',
|
||||
'upload_key' => $this->get_key(),
|
||||
'uploaded_post_id' => $uploaded_attachment_id,
|
||||
'uploaded_video_guid' => $uploaded_video_guid,
|
||||
);
|
||||
} else {
|
||||
// VideoPress attachment is gone, allow user to upload it again.
|
||||
$this->unmark_as_uploaded();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$offset = $this->get_client()->get_offset();
|
||||
$status = false !== $offset ? 'resume' : 'new';
|
||||
$offset = false === $offset ? 0 : $offset;
|
||||
|
||||
return array(
|
||||
'status' => $status,
|
||||
'bytes_uploaded' => $offset,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'upload_key' => $this->get_key(),
|
||||
);
|
||||
} catch ( File_Exception $e ) {
|
||||
return array(
|
||||
'status' => 'resume',
|
||||
'bytes_uploaded' => 0,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'upload_key' => $this->get_key(),
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return array(
|
||||
'status' => 'error',
|
||||
'bytes_uploaded' => -1,
|
||||
'file_size' => $this->get_file_size(),
|
||||
'file_name' => $this->get_file_name(),
|
||||
'message' => $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* The Utils class.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* The Utils class.
|
||||
*/
|
||||
class Utils {
|
||||
/**
|
||||
* Build a VideoPress video URL based on the guid and block attributes.
|
||||
*
|
||||
* @param string $guid - Video GUID.
|
||||
* @param array $attributes - Video block attributes. Default is an empty array.
|
||||
*
|
||||
* @return string VideoPress video URL with the specified attributes.
|
||||
*/
|
||||
public static function get_video_press_url( $guid, $attributes = array() ) {
|
||||
if ( ! $guid ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$video_press_url_options = wp_parse_args(
|
||||
$attributes,
|
||||
array(
|
||||
'autoplay' => false,
|
||||
'controls' => true,
|
||||
'loop' => false,
|
||||
'muted' => false,
|
||||
'playsinline' => false,
|
||||
'poster' => '',
|
||||
'preload' => 'metadata',
|
||||
'seekbarColor' => '',
|
||||
'seekbarPlayedColor' => '',
|
||||
'seekbarLoadingColor' => '',
|
||||
'useAverageColor' => true,
|
||||
)
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'resizeToParent' => 1,
|
||||
'cover' => 1,
|
||||
'autoPlay' => (int) $video_press_url_options['autoplay'],
|
||||
'controls' => (int) $video_press_url_options['controls'],
|
||||
'loop' => (int) $video_press_url_options['loop'],
|
||||
'muted' => (int) $video_press_url_options['muted'],
|
||||
'persistVolume' => $video_press_url_options['muted'] ? 0 : 1,
|
||||
'playsinline' => (int) $video_press_url_options['playsinline'],
|
||||
'preloadContent' => $video_press_url_options['preload'],
|
||||
'sbc' => $video_press_url_options['seekbarColor'],
|
||||
'sbpc' => $video_press_url_options['seekbarPlayedColor'],
|
||||
'sblc' => $video_press_url_options['seekbarLoadingColor'],
|
||||
'useAverageColor' => (int) $video_press_url_options['useAverageColor'],
|
||||
);
|
||||
|
||||
if ( ! empty( $video_press_url_options['poster'] ) ) {
|
||||
$query_args['posterUrl'] = rawurlencode( $video_press_url_options['poster'] );
|
||||
}
|
||||
|
||||
$url = 'https://videopress.com/v/' . $guid;
|
||||
return add_query_arg( $query_args, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given URL is a VideoPress URL.
|
||||
*
|
||||
* @since x.x.x
|
||||
*
|
||||
* @param mixed $url The URL to check. Non-strings return false.
|
||||
* @return bool True if the URL is a VideoPress URL, false otherwise.
|
||||
*/
|
||||
public static function is_videopress_url( $url ) {
|
||||
return null !== self::extract_videopress_guid_from_url( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the VideoPress guid from a recognised VideoPress URL.
|
||||
*
|
||||
* @param mixed $url The URL to inspect. Non-strings return null.
|
||||
* @return string|null The 8-character guid, or null if the URL is not a recognised VideoPress URL.
|
||||
*/
|
||||
public static function extract_videopress_guid_from_url( $url ) {
|
||||
if ( ! is_string( $url ) ) {
|
||||
return null;
|
||||
}
|
||||
$pattern = '/^https?:\/\/(?:(?:v(?:ideo)?\.wordpress\.com|videopress\.com)\/(?:v|embed)|v\.wordpress\.com|videos\.(?:videopress\.com|files\.wordpress\.com))\/([a-z\d]{8})(\/|\b)/i'; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
|
||||
if ( preg_match( $pattern, $url, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress video block email renderer class.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
/**
|
||||
* Handles rendering VideoPress video blocks for email contexts.
|
||||
*/
|
||||
class Video_Block_Email_Renderer {
|
||||
|
||||
/**
|
||||
* Render VideoPress video block for email.
|
||||
*
|
||||
* @param string $block_content The original block HTML content.
|
||||
* @param array $parsed_block The parsed block data including attributes.
|
||||
* @param object $rendering_context Email rendering context.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function render( $block_content, $parsed_block, $rendering_context ) {
|
||||
// Validate input parameters and required dependencies.
|
||||
if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) ||
|
||||
! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Embed' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attributes = $parsed_block['attrs'];
|
||||
|
||||
// Get the VideoPress URL from the guid attribute.
|
||||
$videopress_url = self::get_videopress_url( $attributes );
|
||||
|
||||
if ( empty( $videopress_url ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// For private videos, render a simple link to the post since the video isn't accessible on VideoPress.
|
||||
// The isPrivate attribute is pre-computed by the block editor based on video and site settings.
|
||||
if ( isset( $attributes['isPrivate'] ) && true === $attributes['isPrivate'] ) {
|
||||
return self::render_link( $parsed_block );
|
||||
}
|
||||
|
||||
// Create a mock embed block structure that WooCommerce's embed renderer can handle.
|
||||
// The embed renderer will detect VideoPress from the URL and render it appropriately.
|
||||
$mock_embed_block = array(
|
||||
'blockName' => 'core/embed',
|
||||
'attrs' => array(
|
||||
'url' => $videopress_url,
|
||||
'providerNameSlug' => 'videopress',
|
||||
),
|
||||
'innerHTML' => sprintf(
|
||||
'<figure class="wp-block-embed is-type-video is-provider-videopress"><div class="wp-block-embed__wrapper">%s</div></figure>',
|
||||
esc_url( $videopress_url )
|
||||
),
|
||||
);
|
||||
|
||||
// Preserve email_attrs if present (used for spacing/width calculations).
|
||||
if ( ! empty( $parsed_block['email_attrs'] ) ) {
|
||||
$mock_embed_block['email_attrs'] = $parsed_block['email_attrs'];
|
||||
}
|
||||
|
||||
// Use WooCommerce's core embed renderer.
|
||||
// @phan-suppress-next-line PhanUndeclaredClassMethod -- Optional WooCommerce dependency, checked with class_exists() above.
|
||||
$woo_embed_renderer = new \Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Embed();
|
||||
|
||||
// @phan-suppress-next-line PhanUndeclaredClassMethod -- Optional WooCommerce dependency, checked with class_exists() above.
|
||||
return $woo_embed_renderer->render( $mock_embed_block['innerHTML'], $mock_embed_block, $rendering_context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a simple link for private VideoPress videos in emails.
|
||||
* Links to the post containing the video since private videos aren't accessible on VideoPress.
|
||||
*
|
||||
* @param array $parsed_block The parsed block data.
|
||||
*
|
||||
* @return string The rendered link HTML.
|
||||
*/
|
||||
private static function render_link( $parsed_block ) {
|
||||
$post_url = get_the_permalink();
|
||||
|
||||
if ( empty( $post_url ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$link_text = __( 'Visit the post to watch the video', 'jetpack-videopress-pkg' );
|
||||
|
||||
$link_html = sprintf(
|
||||
'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
|
||||
esc_url( $post_url ),
|
||||
esc_html( $link_text )
|
||||
);
|
||||
|
||||
// Wrap with spacing if email_attrs are present.
|
||||
$email_attrs = $parsed_block['email_attrs'] ?? array();
|
||||
if ( ! empty( $email_attrs['padding'] ) ) {
|
||||
$link_html = sprintf(
|
||||
'<p style="padding: %s;">%s</p>',
|
||||
esc_attr( $email_attrs['padding'] ),
|
||||
$link_html
|
||||
);
|
||||
}
|
||||
|
||||
return $link_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the VideoPress URL from block attributes for email rendering.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
*
|
||||
* @return string VideoPress URL or empty string.
|
||||
*/
|
||||
private static function get_videopress_url( $attributes ) {
|
||||
// Construct URL from guid attribute.
|
||||
if ( ! empty( $attributes['guid'] ) ) {
|
||||
$guid = $attributes['guid'];
|
||||
|
||||
// VideoPress guids are alphanumeric only (e.g., "nfbj0J36").
|
||||
// Validate format to prevent any injection issues.
|
||||
if ( preg_match( '/^[a-zA-Z0-9]+$/', $guid ) ) {
|
||||
return 'https://videopress.com/v/' . $guid;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Features Endpoint
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\My_Jetpack\Product;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* VideoPress REST API class for fetching site features from WPCOM.
|
||||
*/
|
||||
class VideoPress_Rest_Api_V1_Features {
|
||||
/**
|
||||
* Initializes the endpoints
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( static::class, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'features',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => static::class . '::get_features',
|
||||
'permission_callback' => static::class . '::permissions_callback',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has permissions to see the features.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns VideoPress feature flags fetched fresh from WPCOM.
|
||||
*
|
||||
* Uses My Jetpack's Product::get_site_features_from_wpcom() which calls
|
||||
* WPCOM /sites/%d/features API with a 15-second transient cache.
|
||||
*
|
||||
* @return \WP_REST_Response The response object.
|
||||
*/
|
||||
public static function get_features() {
|
||||
$features = Product::get_site_features_from_wpcom();
|
||||
|
||||
if ( is_wp_error( $features ) ) {
|
||||
return rest_ensure_response( $features );
|
||||
}
|
||||
|
||||
$active = $features['active'] ?? array();
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'isVideoPressSupported' => true, // Always true due to free tier.
|
||||
// Check videopress-1tb-storage (Jetpack) or videopress (WordPress.com).
|
||||
'isVideoPress1TBSupported' => in_array( 'videopress-1tb-storage', $active, true )
|
||||
|| ( ( new Host() )->is_wpcom_platform() && in_array( 'videopress', $active, true ) ),
|
||||
'isVideoPressUnlimitedSupported' => in_array( 'videopress-unlimited-storage', $active, true ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Settings Endpoint
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* Rest API class for fetching and setting site settings related to VideoPress.
|
||||
*/
|
||||
class VideoPress_Rest_Api_V1_Settings {
|
||||
/**
|
||||
* Initializes the endpoints
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( static::class, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'settings',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( static::class, 'get_settings' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( static::class, 'update_settings' ),
|
||||
'permission_callback' => function () {
|
||||
return Data::can_perform_action() && current_user_can( 'manage_options' );
|
||||
},
|
||||
'args' => array(
|
||||
'videopress_videos_private_for_site' => array(
|
||||
'description' => __( 'If the VideoPress videos should be private by default', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'boolean',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the VideoPress settings.
|
||||
*
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function get_settings() {
|
||||
$has_connected_owner = Data::has_connected_owner();
|
||||
if ( ! $has_connected_owner ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'owner_not_connected',
|
||||
'User not connected.',
|
||||
array(
|
||||
'code' => 503,
|
||||
'connect_url' => Admin_UI::get_admin_page_url(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$blog_id = Data::get_blog_id();
|
||||
if ( ! $blog_id ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
|
||||
);
|
||||
}
|
||||
|
||||
$status = 200;
|
||||
$data = Data::get_videopress_settings();
|
||||
|
||||
return rest_ensure_response(
|
||||
new WP_REST_Response( $data, $status )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value of the VideoPress settings when a new value
|
||||
* is present on the request body.
|
||||
*
|
||||
* @param WP_REST_Request $request the request object.
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function update_settings( $request ) {
|
||||
$has_connected_owner = Data::has_connected_owner();
|
||||
if ( ! $has_connected_owner ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'owner_not_connected',
|
||||
'User not connected.',
|
||||
array(
|
||||
'code' => 503,
|
||||
'connect_url' => Admin_UI::get_admin_page_url(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$blog_id = Data::get_blog_id();
|
||||
if ( ! $blog_id ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
|
||||
);
|
||||
}
|
||||
|
||||
$json_params = $request->get_json_params();
|
||||
|
||||
// We are sure that the param is set because it's required by the request
|
||||
update_option( 'videopress_private_enabled_for_site', boolval( $json_params['videopress_videos_private_for_site'] ) );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => __( 'VideoPress settings updated successfully.', 'jetpack-videopress-pkg' ),
|
||||
'data' => 200,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Site Info Endpoint
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* VideoPress rest api class for fetching site information
|
||||
*/
|
||||
class VideoPress_Rest_Api_V1_Site {
|
||||
/**
|
||||
* Initializes the endpoints
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( static::class, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'site',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => static::class . '::get_site_info',
|
||||
'permission_callback' => static::class . '::permissions_callback',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether the user have permissions to see the site info
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
return current_user_can( 'read' ); // TODO: confirm this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the site information usually provided by Jetpack, without relying on Jetpack
|
||||
*
|
||||
* @return WP_Rest_Response The response object.
|
||||
*/
|
||||
public static function get_site_info() {
|
||||
$data = Site::get_site_info();
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Stats Endpoints
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* VideoPress stats rest api class
|
||||
*/
|
||||
class VideoPress_Rest_Api_V1_Stats {
|
||||
/**
|
||||
* Initializes the endpoints
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'rest_api_init', array( static::class, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
/**
|
||||
* Basic stats.
|
||||
*/
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'stats',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => static::class . '::get_stats',
|
||||
'permission_callback' => static::class . '::permissions_callback',
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Stats to be featured by My Jetpack.
|
||||
*/
|
||||
register_rest_route(
|
||||
'videopress/v1',
|
||||
'stats/featured',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => static::class . '::get_featured_stats',
|
||||
'permission_callback' => static::class . '::permissions_callback',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether the user have permissions to see stats
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function permissions_callback() {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for getting the general VideoPress stats for the site.
|
||||
*
|
||||
* Returns the plays for all the videos, for today and since the beggining of times.
|
||||
*
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function get_stats() {
|
||||
$today_plays = Stats::get_today_plays();
|
||||
|
||||
if ( is_wp_error( $today_plays ) ) {
|
||||
// TODO: Improve status code.
|
||||
return rest_ensure_response( $today_plays );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'plays' => array(
|
||||
'today' => $today_plays,
|
||||
),
|
||||
);
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the stats that will be featured by My Jetpack.
|
||||
*
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function get_featured_stats() {
|
||||
$featured_stats = Stats::get_featured_stats();
|
||||
return rest_ensure_response( $featured_stats );
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Token
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Jetpack_Options;
|
||||
/**
|
||||
* VideoPress token utility class
|
||||
*/
|
||||
class VideoPressToken {
|
||||
/**
|
||||
* Check if user is connected.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Upload_Exception - If user is not connected.
|
||||
*/
|
||||
private static function check_connection() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
return true;
|
||||
}
|
||||
if ( ! ( new Connection_Manager() )->has_connected_owner() ) {
|
||||
throw new Upload_Exception( __( 'You need to connect Jetpack before being able to upload a video to VideoPress.', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current blog id.
|
||||
*
|
||||
* @return string - Blog id.
|
||||
*/
|
||||
public static function blog_id() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$blog_id = get_current_blog_id();
|
||||
} else {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
}
|
||||
|
||||
return $blog_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a Playback JWT via WPCOM api.
|
||||
*
|
||||
* @param string $guid The VideoPress GUID.
|
||||
* @return string
|
||||
*/
|
||||
public static function videopress_playback_jwt( $guid ) {
|
||||
$blog_id = static::blog_id();
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
);
|
||||
|
||||
$endpoint = "sites/{$blog_id}/media/videopress-playback-jwt/{$guid}";
|
||||
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, 'v2', $args, null, 'wpcom' );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['metadata_token'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['metadata_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a One Time Upload Token via WPCOM api.
|
||||
*
|
||||
* @return string
|
||||
* @throws Upload_Exception If token is empty or is had an error.
|
||||
*/
|
||||
public static function videopress_onetime_upload_token() {
|
||||
if ( static::check_connection() ) {
|
||||
$blog_id = static::blog_id();
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
);
|
||||
|
||||
$endpoint = "sites/{$blog_id}/media/token";
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $args );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
throw new Upload_Exception( __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['upload_token'] ) ) {
|
||||
throw new Upload_Exception( __( 'Could not obtain a VideoPress upload token. Please try again later.', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
|
||||
return $response['upload_token'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress Upload JWT
|
||||
*
|
||||
* @return string
|
||||
* @throws Upload_Exception - If user is not connected, if token is empty or failed to obtain.
|
||||
*/
|
||||
public static function videopress_upload_jwt() {
|
||||
if ( static::check_connection() ) {
|
||||
$blog_id = static::blog_id();
|
||||
$endpoint = "sites/{$blog_id}/media/videopress-upload-jwt";
|
||||
$args = array( 'method' => 'POST' );
|
||||
$result = Client::wpcom_json_api_request_as_blog( $endpoint, 'v2', $args, null, 'wpcom' );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
throw new Upload_Exception(
|
||||
__( 'Could not obtain a VideoPress upload JWT. Please try again later.', 'jetpack-videopress-pkg' ) .
|
||||
'(' . $result->get_error_message() . ')'
|
||||
);
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
if ( empty( $response['upload_token'] ) ) {
|
||||
throw new Upload_Exception( __( 'Could not obtain a VideoPress upload JWT. Please try again later. (empty upload token)', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
|
||||
return $response['upload_token'];
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Extend the REST API functionality for VideoPress users.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
* @since-jetpack 7.1.0
|
||||
* @since 0.3.0
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
|
||||
use WP_Post;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* Add per-attachment VideoPress data.
|
||||
*
|
||||
* { # Attachment Object
|
||||
* ...
|
||||
* jetpack_videopress_guid: (string) VideoPress identifier
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Attachment_VideoPress_Field {
|
||||
/**
|
||||
* The REST Object Type to which the jetpack_videopress_guid field will be added.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $object_type = 'attachment';
|
||||
|
||||
/**
|
||||
* The name of the REST API field to add.
|
||||
*
|
||||
* @var string $field_name
|
||||
*/
|
||||
protected $field_name = 'jetpack_videopress_guid';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_fields' ) );
|
||||
|
||||
// do this again later to collect any CPTs that get registered later.
|
||||
add_action( 'restapi_theme_init', array( $this, 'register_fields' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the jetpack_videopress field and adds a filter to remove it for attachments that are not videos.
|
||||
*/
|
||||
public function register_fields() {
|
||||
global $wp_rest_additional_fields;
|
||||
|
||||
if ( ! empty( $wp_rest_additional_fields[ $this->object_type ][ $this->field_name ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_rest_field(
|
||||
$this->object_type,
|
||||
$this->field_name,
|
||||
array(
|
||||
'get_callback' => array( $this, 'get' ),
|
||||
'update_callback' => null,
|
||||
'schema' => $this->get_schema(),
|
||||
)
|
||||
);
|
||||
|
||||
add_filter( 'rest_prepare_attachment', array( $this, 'remove_field_for_non_videos' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines data structure and what elements are visible in which contexts.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_schema() {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => $this->field_name,
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'description' => __( 'Unique VideoPress ID', 'jetpack-videopress-pkg' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter: Retrieve current VideoPress data for a given attachment.
|
||||
*
|
||||
* @param array $attachment Response from the attachment endpoint.
|
||||
* @param WP_REST_Request $request Request to the attachment endpoint.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get( $attachment, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! isset( $attachment['id'] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$blog_id = Jetpack_Connection::get_site_id();
|
||||
if ( ! is_int( $blog_id ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$videopress_guid = $this->get_videopress_guid( (int) $attachment['id'], $blog_id );
|
||||
|
||||
if ( ! $videopress_guid ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$schema = $this->get_schema();
|
||||
$is_valid = rest_validate_value_from_schema( $videopress_guid, $schema, $this->field_name );
|
||||
if ( is_wp_error( $is_valid ) ) {
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
return $videopress_guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress GUID for a given attachment.
|
||||
*
|
||||
* This is pulled out into a separate method to support unit test mocking.
|
||||
*
|
||||
* @param int $attachment_id Attachment ID.
|
||||
* @param int $blog_id Blog ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_videopress_guid( $attachment_id, $blog_id ) {
|
||||
return video_get_info_by_blogpostid( $blog_id, $attachment_id )->guid ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given attachment is a video.
|
||||
*
|
||||
* @param object $attachment The attachment object.
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function is_video( $attachment ) {
|
||||
return wp_startswith( $attachment->post_mime_type, 'video/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jetpack_videopress_guid field from the response if the
|
||||
* given attachment is not a video.
|
||||
*
|
||||
* @param WP_REST_Response $response Response from the attachment endpoint.
|
||||
* @param WP_Post $attachment The original attachment object.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function remove_field_for_non_videos( $response, $attachment ) {
|
||||
if ( ! $this->is_video( $attachment ) ) {
|
||||
unset( $response->data[ $this->field_name ] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Attachment_VideoPress_Field' );
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/**
|
||||
* Extend the REST API functionality for VideoPress users.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
* @since-jetpack 7.1.0
|
||||
* @since 0.3.1
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
|
||||
use WP_Post;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* Add per-attachment VideoPress data.
|
||||
*
|
||||
* { # Attachment Object
|
||||
* ...
|
||||
* jetpack_videopress: (object) VideoPress data
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @since 7.1.0
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Attachment_VideoPress_Data {
|
||||
/**
|
||||
* The REST Object Type to which the jetpack_videopress field will be added.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $object_type = 'attachment';
|
||||
|
||||
/**
|
||||
* The name of the REST API field to add.
|
||||
*
|
||||
* @var string $field_name
|
||||
*/
|
||||
protected $field_name = 'jetpack_videopress';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_fields' ) );
|
||||
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
add_action( 'rest_api_init', array( $this, 'add_jetpack_videopress_custom_query_filters' ) );
|
||||
}
|
||||
|
||||
// do this again later to collect any CPTs that get registered later.
|
||||
add_action( 'restapi_theme_init', array( $this, 'register_fields' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the jetpack_videopress field and adds a filter to remove it for attachments that are not videos.
|
||||
*/
|
||||
public function register_fields() {
|
||||
global $wp_rest_additional_fields;
|
||||
|
||||
if ( ! empty( $wp_rest_additional_fields[ $this->object_type ][ $this->field_name ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_rest_field(
|
||||
$this->object_type,
|
||||
$this->field_name,
|
||||
array(
|
||||
'get_callback' => array( $this, 'get' ),
|
||||
'update_callback' => null,
|
||||
'schema' => $this->get_schema(),
|
||||
)
|
||||
);
|
||||
|
||||
add_filter( 'rest_prepare_attachment', array( $this, 'remove_field_for_non_videos' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the custom query filters
|
||||
*/
|
||||
public function add_jetpack_videopress_custom_query_filters() {
|
||||
add_filter( 'rest_attachment_query', array( $this, 'filter_attachments_by_jetpack_videopress_fields' ), 999, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter request args to handle the custom VideoPress query filters
|
||||
*
|
||||
* Possible filters:
|
||||
*
|
||||
* `no_videopress`: the returned attachments should not have a videopress_guid
|
||||
*
|
||||
* @param array $args The original list of args before the filtering.
|
||||
* @param WP_REST_Request $request The original request data.
|
||||
*/
|
||||
public function filter_attachments_by_jetpack_videopress_fields( $args, $request ) {
|
||||
|
||||
if ( ! isset( $args['meta_query'] ) || ! is_array( $args['meta_query'] ) ) {
|
||||
$args['meta_query'] = array();
|
||||
}
|
||||
|
||||
/* To ignore all VideoPress videos, select only attachments without videopress_guid meta field */
|
||||
if ( isset( $request['no_videopress'] ) ) {
|
||||
$args['meta_query'][] = array(
|
||||
'key' => 'videopress_guid',
|
||||
'compare' => 'NOT EXISTS',
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Hide local attachments that have already been uploaded to VideoPress.
|
||||
* Such "zombie" locals carry a `_videopress_uploaded_id` meta pointing
|
||||
* at their VideoPress sibling attachment; the sibling is the row the
|
||||
* dashboard should surface.
|
||||
*/
|
||||
if ( isset( $request['videopress_hide_already_uploaded'] ) ) {
|
||||
$args['meta_query'][] = array(
|
||||
'key' => Uploader::UPLOADED_KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
);
|
||||
}
|
||||
|
||||
/* Filter using privacy setting meta key */
|
||||
if ( isset( $request['videopress_privacy_setting'] ) ) {
|
||||
$videopress_privacy_setting = sanitize_text_field( $request['videopress_privacy_setting'] );
|
||||
|
||||
/* Allows the filtering to happens using a list of privacy settings separated by comma */
|
||||
$videopress_privacy_setting_list = explode( ',', $videopress_privacy_setting );
|
||||
|
||||
$site_default_is_private = Data::get_videopress_videos_private_for_site();
|
||||
|
||||
if ( $site_default_is_private ) {
|
||||
/**
|
||||
* If the search is looking for private videos and the site default is private,
|
||||
* the site default setting should be included on the search.
|
||||
*/
|
||||
if ( in_array( strval( \VIDEOPRESS_PRIVACY::IS_PRIVATE ), $videopress_privacy_setting_list, true ) ) {
|
||||
$videopress_privacy_setting_list[] = \VIDEOPRESS_PRIVACY::SITE_DEFAULT;
|
||||
}
|
||||
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
|
||||
/**
|
||||
* If the search is looking for public videos and the site default is public,
|
||||
* the site default setting should be included on the search.
|
||||
*/
|
||||
if ( in_array( strval( \VIDEOPRESS_PRIVACY::IS_PUBLIC ), $videopress_privacy_setting_list, true ) ) {
|
||||
$videopress_privacy_setting_list[] = \VIDEOPRESS_PRIVACY::SITE_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
$args['meta_query'][] = array(
|
||||
'key' => 'videopress_privacy_setting',
|
||||
'value' => $videopress_privacy_setting_list,
|
||||
'compare' => 'IN',
|
||||
);
|
||||
}
|
||||
|
||||
/* Filter using rating meta key */
|
||||
if ( isset( $request['videopress_rating'] ) ) {
|
||||
$videopress_rating = sanitize_text_field( $request['videopress_rating'] );
|
||||
|
||||
/* Allows the filtering to happens using a list of ratings separated by comma */
|
||||
$videopress_rating_list = explode( ',', $videopress_rating );
|
||||
|
||||
$args['meta_query'][] = array(
|
||||
'key' => 'videopress_rating',
|
||||
'value' => $videopress_rating_list,
|
||||
'compare' => 'IN',
|
||||
);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines data structure and what elements are visible in which contexts
|
||||
*/
|
||||
public function get_schema() {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => $this->field_name,
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'description' => __( 'VideoPress Data', 'jetpack-videopress-pkg' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter: Retrieve current VideoPress data for a given attachment.
|
||||
*
|
||||
* @param array $attachment Response from the attachment endpoint.
|
||||
* @param WP_REST_Request $request Request to the attachment endpoint.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get( $attachment, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! isset( $attachment['id'] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$blog_id = Jetpack_Connection::get_site_id();
|
||||
if ( ! is_int( $blog_id ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$videopress = $this->get_videopress_data( (int) $attachment['id'], $blog_id );
|
||||
|
||||
if ( ! $videopress ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $videopress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the VideoPress GUID for a given attachment.
|
||||
*
|
||||
* This is pulled out into a separate method to support unit test mocking.
|
||||
*
|
||||
* @param int $attachment_id Attachment ID.
|
||||
* @param int $blog_id Blog ID.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_videopress_data( $attachment_id, $blog_id ) {
|
||||
$info = video_get_info_by_blogpostid( $blog_id, $attachment_id );
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$title = video_get_title( $blog_id, $attachment_id );
|
||||
$description = video_get_description( $blog_id, $attachment_id );
|
||||
|
||||
$video_attachment = get_blog_post( $blog_id, $attachment_id );
|
||||
if ( null === $video_attachment ) {
|
||||
$caption = '';
|
||||
} else {
|
||||
$caption = $video_attachment->post_excerpt;
|
||||
}
|
||||
} else {
|
||||
$title = $info->title;
|
||||
$description = $info->description;
|
||||
$caption = $info->caption;
|
||||
}
|
||||
|
||||
$video_privacy_setting = ! isset( $info->privacy_setting ) ? \VIDEOPRESS_PRIVACY::SITE_DEFAULT : intval( $info->privacy_setting );
|
||||
$private_enabled_for_site = Data::get_videopress_videos_private_for_site();
|
||||
$is_private = $this->video_is_private( $video_privacy_setting, $private_enabled_for_site );
|
||||
|
||||
// The video needs a playback token if it's private for any reason (video privacy setting or site default privacy setting)
|
||||
$video_needs_playback_token = $is_private;
|
||||
|
||||
return array(
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'caption' => $caption,
|
||||
'guid' => $info->guid ?? null,
|
||||
'rating' => $info->rating ?? null,
|
||||
'allow_download' =>
|
||||
isset( $info->allow_download ) && $info->allow_download ? 1 : 0,
|
||||
'display_embed' =>
|
||||
isset( $info->display_embed ) && $info->display_embed ? 1 : 0,
|
||||
'privacy_setting' => $video_privacy_setting,
|
||||
'needs_playback_token' => $video_needs_playback_token,
|
||||
'is_private' => $is_private,
|
||||
'private_enabled_for_site' => $private_enabled_for_site,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given attachment is a video.
|
||||
*
|
||||
* @param object $attachment The attachment object.
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function is_video( $attachment ) {
|
||||
return isset( $attachment->post_mime_type ) && wp_startswith( $attachment->post_mime_type, 'video/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jetpack_videopress field from the response if the
|
||||
* given attachment is not a video.
|
||||
*
|
||||
* @param WP_REST_Response $response Response from the attachment endpoint.
|
||||
* @param WP_Post $attachment The original attachment object.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function remove_field_for_non_videos( $response, $attachment ) {
|
||||
if ( ! $this->is_video( $attachment ) ) {
|
||||
unset( $response->data[ $this->field_name ] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a video is private based on the video privacy
|
||||
* setting and the site default privacy setting.
|
||||
*
|
||||
* @param int $video_privacy_setting The privacy setting for the video.
|
||||
* @param bool $private_enabled_for_site Flag stating if the default video privacy is private.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function video_is_private( $video_privacy_setting, $private_enabled_for_site ) {
|
||||
if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PUBLIC ) {
|
||||
return false;
|
||||
}
|
||||
if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PRIVATE ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $private_enabled_for_site;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Attachment_VideoPress_Data' );
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for managing VideoPress metadata.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since-jetpack 9.3.0
|
||||
* @since 0.1.3
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use WP_Error;
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* VideoPress wpcom api v2 endpoint
|
||||
*
|
||||
* @phan-constructor-used-for-side-effects
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_VideoPress extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'videopress';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// Meta Route.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/meta',
|
||||
array(
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'The post id for the attachment.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
),
|
||||
'title' => array(
|
||||
'description' => __( 'The title of the video.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'description' => array(
|
||||
'description' => __( 'The description of the video.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
'caption' => array(
|
||||
'description' => __( 'The caption of the video.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
'rating' => array(
|
||||
'description' => __( 'The video content rating. One of G, PG-13 or R-17', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'display_embed' => array(
|
||||
'description' => __( 'Display the share menu in the player.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'allow_download' => array(
|
||||
'description' => __( 'Display download option and allow viewers to download this video', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'privacy_setting' => array(
|
||||
'description' => __( 'How to determine if the video should be public or private', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
'enum' => array(
|
||||
\VIDEOPRESS_PRIVACY::IS_PUBLIC,
|
||||
\VIDEOPRESS_PRIVACY::IS_PRIVATE,
|
||||
\VIDEOPRESS_PRIVACY::SITE_DEFAULT,
|
||||
),
|
||||
),
|
||||
),
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'videopress_block_update_meta' ),
|
||||
'permission_callback' => function () {
|
||||
return Data::can_perform_action() && current_user_can( 'edit_posts' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Poster Route.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<video_guid>[A-Za-z0-9]{8})/poster',
|
||||
array(
|
||||
'args' => array(
|
||||
'video_guid' => array(
|
||||
'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ), // @phan-suppress-current-line PhanPluginMixedKeyNoKey
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'videopress_block_get_poster' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'read' );
|
||||
},
|
||||
),
|
||||
array(
|
||||
'args' => array(
|
||||
'at_time' => array(
|
||||
'description' => __( 'The time in the video to use as the poster frame.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'is_millisec' => array(
|
||||
'description' => __( 'Whether the time is in milliseconds or seconds.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'poster_attachment_id' => array(
|
||||
'description' => __( 'The attachment id of the poster image.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'videopress_block_update_poster' ),
|
||||
'permission_callback' => function () {
|
||||
return Data::can_perform_action() && current_user_can( 'upload_files' );
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Endpoint to know if the video metadata is editable.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<video_guid>[A-Za-z0-9]{8})/check-ownership/(?P<post_id>\d+)/',
|
||||
array(
|
||||
'args' => array(
|
||||
'video_guid' => array(
|
||||
'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ), // @phan-suppress-current-line PhanPluginMixedKeyNoKey
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'post_id' => array(
|
||||
'description' => __( 'The post id for the attachment.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'videopress_video_belong_to_site' ),
|
||||
'permission_callback' => function () {
|
||||
return Data::can_perform_action() && current_user_can( 'upload_files' );
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Token Route.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/upload-jwt',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'videopress_upload_jwt' ),
|
||||
'permission_callback' => function () {
|
||||
return Data::can_perform_action() && current_user_can( 'upload_files' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Playback Token Route.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/playback-jwt/(?P<video_guid>[A-Za-z0-9]{8})',
|
||||
array(
|
||||
'args' => array(
|
||||
'video_guid' => array(
|
||||
'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'videopress_playback_jwt' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'read' );
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the video belongs to the current site,
|
||||
* considering the given post_id and the video_guid.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_REST_Response True if the video belongs to the current site, false otherwise.
|
||||
*/
|
||||
public function videopress_video_belong_to_site( $request ) {
|
||||
$post_id = $request->get_param( 'post_id' );
|
||||
$video_guid = $request->get_param( 'video_guid' );
|
||||
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
$found_guid = get_post_meta( $post_id, 'videopress_guid', true );
|
||||
} else {
|
||||
$blog_id = get_current_blog_id();
|
||||
$info = video_get_info_by_blogpostid( $blog_id, $post_id );
|
||||
$found_guid = $info ? $info->guid : '';
|
||||
}
|
||||
|
||||
if ( ! $found_guid ) {
|
||||
return rest_ensure_response( array( 'video-belong-to-site' => false ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( array( 'video-belong-to-site' => $found_guid === $video_guid ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit WPCOM poster endpoint.
|
||||
*
|
||||
* @param string $video_guid The VideoPress GUID.
|
||||
* @param array $args Request args.
|
||||
* @param array $body Request body.
|
||||
* @param string $query Request query.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function wpcom_poster_request( $video_guid, $args, $body = null, $query = '' ) {
|
||||
$query = $query !== '' ? '?' . $query : '';
|
||||
$endpoint = 'videos/' . $video_guid . '/poster' . $query;
|
||||
|
||||
$url = sprintf(
|
||||
'%s/%s/v%s/%s',
|
||||
Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
|
||||
'rest',
|
||||
'1.1',
|
||||
$endpoint
|
||||
);
|
||||
|
||||
$request_args = array_merge( $args, array( 'body' => $body ) );
|
||||
|
||||
// @phan-suppress-next-line PhanAccessMethodInternal -- Phan is correct, but the usage is intentional.
|
||||
$result = Client::_wp_remote_request( $url, $request_args );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
|
||||
$response = $result['http_response'];
|
||||
|
||||
$status = $response->get_status();
|
||||
|
||||
$data = array(
|
||||
'code' => $status,
|
||||
'data' => json_decode( $response->get_data(), true ),
|
||||
);
|
||||
|
||||
return rest_ensure_response(
|
||||
new WP_REST_Response( $data, $status )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the a poster image via the WPCOM REST API.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function videopress_block_update_poster( $request ) {
|
||||
try {
|
||||
$blog_id = VideoPressToken::blog_id();
|
||||
$token = VideoPressToken::videopress_onetime_upload_token();
|
||||
$video_guid = $request->get_param( 'video_guid' );
|
||||
$json_params = $request->get_json_params();
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'headers' => array(
|
||||
'content-type' => 'application/json',
|
||||
'Authorization' => 'X_UPLOAD_TOKEN token="' . $token . '" blog_id="' . $blog_id . '"',
|
||||
),
|
||||
);
|
||||
|
||||
return $this->wpcom_poster_request(
|
||||
$video_guid,
|
||||
$args,
|
||||
wp_json_encode( $json_params, JSON_UNESCAPED_SLASHES )
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( 'videopress_block_update_poster_error', $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a poster image via the WPCOM REST API.
|
||||
*
|
||||
* @param WP_REST_Request $request the request object.
|
||||
* @return object|WP_Error Success object or WP_Error with error details.
|
||||
*/
|
||||
public function videopress_block_get_poster( $request ) {
|
||||
$video_guid = $request->get_param( 'video_guid' );
|
||||
$jwt = VideoPressToken::videopress_playback_jwt( $video_guid );
|
||||
|
||||
$args = array(
|
||||
'method' => 'GET',
|
||||
);
|
||||
|
||||
return $this->wpcom_poster_request(
|
||||
$video_guid,
|
||||
$args,
|
||||
null,
|
||||
'metadata_token=' . $jwt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for getting the VideoPress Upload JWT
|
||||
*
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function videopress_upload_jwt() {
|
||||
$has_connected_owner = Data::has_connected_owner();
|
||||
if ( ! $has_connected_owner ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'owner_not_connected',
|
||||
'User not connected.',
|
||||
array(
|
||||
'code' => 503,
|
||||
'connect_url' => Admin_UI::get_admin_page_url(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$blog_id = Data::get_blog_id();
|
||||
if ( ! $blog_id ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$token = VideoPressToken::videopress_upload_jwt();
|
||||
$status = 200;
|
||||
$data = array(
|
||||
'upload_token' => $token,
|
||||
'upload_url' => videopress_make_resumable_upload_path( $blog_id ),
|
||||
'upload_blog_id' => $blog_id,
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
$status = 500;
|
||||
$data = array(
|
||||
'error' => $e->getMessage(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
new WP_REST_Response( $data, $status )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for generating a VideoPress Playback JWT
|
||||
*
|
||||
* @param WP_REST_Request $request the request object.
|
||||
* @return WP_Rest_Response - The response object.
|
||||
*/
|
||||
public static function videopress_playback_jwt( $request ) {
|
||||
$has_connected_owner = Data::has_connected_owner();
|
||||
if ( ! $has_connected_owner ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'owner_not_connected',
|
||||
'User not connected.',
|
||||
array(
|
||||
'code' => 503,
|
||||
'connect_url' => Admin_UI::get_admin_page_url(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$blog_id = Data::get_blog_id();
|
||||
if ( ! $blog_id ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$video_guid = $request->get_param( 'video_guid' );
|
||||
$token = VideoPressToken::videopress_playback_jwt( $video_guid );
|
||||
$status = 200;
|
||||
$data = array(
|
||||
'playback_token' => $token,
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
$status = 500;
|
||||
$data = array(
|
||||
'error' => $e->getMessage(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
new WP_REST_Response( $data, $status )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates attachment meta and video metadata via the WPCOM REST API.
|
||||
*
|
||||
* @param WP_REST_Request $request the request object.
|
||||
* @return object|WP_Error Success object or WP_Error with error details.
|
||||
*/
|
||||
public function videopress_block_update_meta( $request ) {
|
||||
$json_params = $request->get_json_params();
|
||||
$post_id = $json_params['id'];
|
||||
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
$guid = get_post_meta( $post_id, 'videopress_guid', true );
|
||||
} else {
|
||||
$blog_id = get_current_blog_id();
|
||||
$info = video_get_info_by_blogpostid( $blog_id, $post_id );
|
||||
$guid = $info ? $info->guid : '';
|
||||
}
|
||||
|
||||
if ( ! $guid ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'error',
|
||||
__( 'This attachment cannot be updated yet.', 'jetpack-videopress-pkg' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$video_request_params = $json_params;
|
||||
unset( $video_request_params['id'] );
|
||||
$video_request_params['guid'] = $guid;
|
||||
|
||||
$endpoint = 'videos';
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
);
|
||||
|
||||
$result = Client::wpcom_json_api_request_as_blog(
|
||||
$endpoint,
|
||||
'2',
|
||||
$args,
|
||||
wp_json_encode( $video_request_params, JSON_UNESCAPED_SLASHES ),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
|
||||
$response_body = json_decode( wp_remote_retrieve_body( $result ) );
|
||||
if ( is_bool( $response_body ) && $response_body ) {
|
||||
/*
|
||||
* Title, description and caption of the video are not stored as metadata on the attachment,
|
||||
* but as post_content, post_title and post_excerpt on the attachment's post object.
|
||||
* We need to update those fields here, too.
|
||||
*/
|
||||
$post_title = null;
|
||||
if ( isset( $json_params['title'] ) ) {
|
||||
$post_title = sanitize_text_field( $json_params['title'] );
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_title' => $post_title,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$post_content = null;
|
||||
if ( isset( $json_params['description'] ) ) {
|
||||
$post_content = sanitize_textarea_field( $json_params['description'] );
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_content' => $post_content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$post_excerpt = null;
|
||||
if ( isset( $json_params['caption'] ) ) {
|
||||
$post_excerpt = sanitize_textarea_field( $json_params['caption'] );
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_excerpt' => $post_excerpt,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// VideoPress data is stored in attachment meta for Jetpack sites, but not on wpcom.
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
$should_update_meta = false;
|
||||
|
||||
if ( ! $meta ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
'error',
|
||||
__( 'Attachment meta was not found.', 'jetpack-videopress-pkg' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $json_params['display_embed'] ) && isset( $meta['videopress']['display_embed'] ) ) {
|
||||
$meta['videopress']['display_embed'] = $json_params['display_embed'];
|
||||
$should_update_meta = true;
|
||||
}
|
||||
|
||||
if ( isset( $json_params['rating'] ) && isset( $meta['videopress']['rating'] ) && videopress_is_valid_video_rating( $json_params['rating'] ) ) {
|
||||
$meta['videopress']['rating'] = $json_params['rating'];
|
||||
$should_update_meta = true;
|
||||
|
||||
/** Set a new meta field so we can filter using it directly */
|
||||
update_post_meta( $post_id, 'videopress_rating', $json_params['rating'] );
|
||||
}
|
||||
|
||||
if ( isset( $json_params['title'] ) ) {
|
||||
$meta['videopress']['title'] = $post_title;
|
||||
$should_update_meta = true;
|
||||
}
|
||||
|
||||
if ( isset( $json_params['description'] ) ) {
|
||||
$meta['videopress']['description'] = $post_content;
|
||||
$should_update_meta = true;
|
||||
}
|
||||
|
||||
if ( isset( $json_params['caption'] ) ) {
|
||||
$meta['videopress']['caption'] = $post_excerpt;
|
||||
$should_update_meta = true;
|
||||
}
|
||||
|
||||
if ( isset( $json_params['poster'] ) ) {
|
||||
$meta['videopress']['poster'] = $json_params['poster'];
|
||||
$should_update_meta = true;
|
||||
}
|
||||
|
||||
if ( isset( $json_params['allow_download'] ) ) {
|
||||
$allow_download = (bool) $json_params['allow_download'];
|
||||
if ( ! isset( $meta['videopress']['allow_download'] ) || $meta['videopress']['allow_download'] !== $allow_download ) {
|
||||
$meta['videopress']['allow_download'] = $allow_download;
|
||||
$should_update_meta = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $json_params['privacy_setting'] ) ) {
|
||||
$privacy_setting = $json_params['privacy_setting'];
|
||||
if ( ! isset( $meta['videopress']['privacy_setting'] ) || $meta['videopress']['privacy_setting'] !== $privacy_setting ) {
|
||||
$meta['videopress']['privacy_setting'] = $privacy_setting;
|
||||
$should_update_meta = true;
|
||||
|
||||
/** Set a new meta field so we can filter using it directly */
|
||||
update_post_meta( $post_id, 'videopress_privacy_setting', $privacy_setting );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $should_update_meta ) {
|
||||
wp_update_attachment_metadata( $post_id, $meta );
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => __( 'Video meta updated successfully.', 'jetpack-videopress-pkg' ),
|
||||
'data' => 200,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
$response_body->code,
|
||||
$response_body->message,
|
||||
$response_body->data
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Endpoint_VideoPress' );
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* The VIdeoPress XMLRPC class
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* VideoPress playback module markup generator.
|
||||
*
|
||||
* @since 0.1.1
|
||||
*/
|
||||
class XMLRPC {
|
||||
|
||||
/**
|
||||
* Singleton XMLRPC instance.
|
||||
*
|
||||
* @var XMLRPC
|
||||
**/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* The current user object.
|
||||
*
|
||||
* @var WP_User
|
||||
*/
|
||||
private $current_user;
|
||||
|
||||
/**
|
||||
* Private VideoPress_XMLRPC constructor.
|
||||
*
|
||||
* Use the VideoPress_XMLRPC::init() method to get an instance.
|
||||
*/
|
||||
private function __construct() {
|
||||
add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the VideoPress_XMLRPC and get back a singleton instance.
|
||||
*
|
||||
* @return XMLRPC
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new XMLRPC();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional methods the WordPress xmlrpc API for handling VideoPress specific features
|
||||
*
|
||||
* @param array $methods The Jetpack API methods.
|
||||
* @param array $core_methods The WordPress Core API methods (ignored).
|
||||
* @param WP_User $user The user object the API request is signed by.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function xmlrpc_methods( $methods, $core_methods, $user ) {
|
||||
if ( $user && $user instanceof WP_User ) {
|
||||
$this->current_user = $user;
|
||||
}
|
||||
|
||||
$methods['jetpack.createMediaItem'] = array( $this, 'create_media_item' );
|
||||
$methods['jetpack.updateVideoPressMediaItem'] = array( $this, 'update_videopress_media_item' );
|
||||
$methods['jetpack.updateVideoPressPosterImage'] = array( $this, 'update_poster_image' );
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used by the WPCOM VideoPress uploader in order to create a media item with
|
||||
* specific meta data about an uploaded file. After this, the transcoding session will
|
||||
* update the meta information via the update_videopress_media_item() method.
|
||||
*
|
||||
* Note: This method technically handles the creation of multiple media objects, though
|
||||
* in practice this is never done.
|
||||
*
|
||||
* @param array $media Media items being uploaded.
|
||||
* @return array
|
||||
*/
|
||||
public function create_media_item( $media ) {
|
||||
$this->authenticate_user();
|
||||
|
||||
foreach ( $media as & $media_item ) {
|
||||
$title = sanitize_title( basename( $media_item['url'] ) );
|
||||
$guid = $media['guid'] ?? null;
|
||||
|
||||
$media_id = videopress_create_new_media_item( $title, $guid );
|
||||
|
||||
wp_update_attachment_metadata(
|
||||
$media_id,
|
||||
array(
|
||||
'original' => array(
|
||||
'url' => $media_item['url'],
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$media_item['post'] = get_post( $media_id );
|
||||
}
|
||||
|
||||
return array( 'media' => $media );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update VideoPress metadata for a media item.
|
||||
*
|
||||
* @param array $request Media item to update.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update_videopress_media_item( $request ) {
|
||||
$this->authenticate_user();
|
||||
|
||||
$id = $request['post_id'];
|
||||
$status = $request['status'];
|
||||
$format = $request['format'];
|
||||
$info = $request['info'];
|
||||
|
||||
$attachment = get_post( $id );
|
||||
if ( ! $attachment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attachment->guid = $info['original'];
|
||||
$attachment->post_mime_type = 'video/videopress';
|
||||
|
||||
wp_update_post( $attachment );
|
||||
|
||||
// Update the vp guid and set it to a direct meta property.
|
||||
update_post_meta( $id, 'videopress_guid', $info['guid'] );
|
||||
|
||||
$meta = wp_get_attachment_metadata( $id );
|
||||
|
||||
$meta['width'] = $info['width'];
|
||||
$meta['height'] = $info['height'];
|
||||
$meta['original']['url'] = $info['original'];
|
||||
$meta['videopress'] = $info;
|
||||
$meta['videopress']['url'] = 'https://videopress.com/v/' . $info['guid'];
|
||||
|
||||
// Update file statuses
|
||||
if ( ! empty( $format ) ) {
|
||||
$meta['file_statuses'][ $format ] = $status;
|
||||
}
|
||||
|
||||
if ( ! get_post_meta( $id, '_thumbnail_id', true ) ) {
|
||||
// Update the poster in the VideoPress info.
|
||||
$thumbnail_id = videopress_download_poster_image( $info['poster'], $id );
|
||||
|
||||
if ( is_int( $thumbnail_id ) ) {
|
||||
update_post_meta( $id, '_thumbnail_id', $thumbnail_id );
|
||||
}
|
||||
}
|
||||
|
||||
wp_update_attachment_metadata( $id, $meta );
|
||||
|
||||
videopress_update_meta_data( $id );
|
||||
|
||||
// update the meta to tell us that we're processing or complete
|
||||
update_post_meta( $id, 'videopress_status', videopress_is_finished_processing( $id ) ? 'complete' : 'processing' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update poster image for a VideoPress media item.
|
||||
*
|
||||
* @param array $request The media item to update.
|
||||
* @return bool
|
||||
*/
|
||||
public function update_poster_image( $request ) {
|
||||
$this->authenticate_user();
|
||||
|
||||
$post_id = $request['post_id'];
|
||||
$poster = $request['poster'];
|
||||
|
||||
$attachment = get_post( $post_id );
|
||||
if ( ! $attachment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$poster = apply_filters( 'jetpack_photon_url', $poster );
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
$meta['videopress']['poster'] = $poster;
|
||||
wp_update_attachment_metadata( $post_id, $meta );
|
||||
|
||||
// Update the poster in the VideoPress info.
|
||||
$thumbnail_id = videopress_download_poster_image( $poster, $post_id );
|
||||
|
||||
if ( ! is_int( $thumbnail_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_thumbnail_id', $thumbnail_id );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the XML-RPC request is signed by a user token, and authenticate the user in WordPress.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function authenticate_user() {
|
||||
if ( $this->current_user ) {
|
||||
wp_set_current_user( $this->current_user->ID );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/* global jQuery, wp */
|
||||
( function ( $, wp ) {
|
||||
/**
|
||||
* Returns attachment IDs for VideoPress videos that are still processing.
|
||||
*
|
||||
* @return {Array} Attachment IDs.
|
||||
*/
|
||||
function getProcessingVideoIds() {
|
||||
if ( ! wp.media.frame || ! wp.media.frame.state() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const library = wp.media.frame.state().get( 'library' );
|
||||
if ( ! library ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ids = [];
|
||||
library.each( function ( attachment ) {
|
||||
if (
|
||||
attachment.get( 'type' ) === 'video' &&
|
||||
attachment.get( 'subtype' ) === 'videopress' &&
|
||||
attachment.get( 'videopress_status' ) &&
|
||||
attachment.get( 'videopress_status' ) !== 'complete'
|
||||
) {
|
||||
ids.push( attachment.get( 'id' ) );
|
||||
}
|
||||
} );
|
||||
return ids;
|
||||
}
|
||||
|
||||
$( document ).on( 'heartbeat-send', function ( e, data ) {
|
||||
const ids = getProcessingVideoIds();
|
||||
if ( ids.length ) {
|
||||
data.videopress_processing_ids = ids;
|
||||
wp.heartbeat.interval( 'fast' );
|
||||
} else {
|
||||
wp.heartbeat.interval( 'standard' );
|
||||
}
|
||||
} );
|
||||
|
||||
// Speed up heartbeat when a video is uploaded so processing status
|
||||
// is polled quickly. wp.Uploader.queue fires 'add' as soon as an
|
||||
// upload starts, with the file's MIME type available.
|
||||
if ( wp.Uploader ) {
|
||||
wp.Uploader.queue.on( 'add', function ( attachment ) {
|
||||
const file = attachment.get( 'file' );
|
||||
if ( file && /^video\//.test( file.type ) ) {
|
||||
wp.heartbeat.interval( 'fast' );
|
||||
wp.heartbeat.connectNow();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Also kick off fast polling if videos were already processing on
|
||||
// page load. The media frame isn't available immediately, so poll
|
||||
// until it is.
|
||||
const bootCheck = setInterval( function () {
|
||||
if ( ! wp.media.frame || ! wp.media.frame.state() ) {
|
||||
return;
|
||||
}
|
||||
clearInterval( bootCheck );
|
||||
|
||||
if ( getProcessingVideoIds().length ) {
|
||||
wp.heartbeat.interval( 'fast' );
|
||||
}
|
||||
}, 500 );
|
||||
|
||||
$( document ).on( 'heartbeat-tick', function ( e, data ) {
|
||||
if ( ! data.videopress_processing_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp.media.frame || ! wp.media.frame.state() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const library = wp.media.frame.state().get( 'library' );
|
||||
if ( ! library ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.each( data.videopress_processing_status, function ( id, status ) {
|
||||
const attachment = library.get( id );
|
||||
if ( attachment && status === 'complete' ) {
|
||||
// Prevent duplicate fetches on subsequent ticks.
|
||||
attachment.set( 'videopress_status', 'complete' );
|
||||
|
||||
wp.ajax
|
||||
.send( 'get-attachment', {
|
||||
data: { id: id },
|
||||
} )
|
||||
.done( function ( attrs ) {
|
||||
attachment.set( attrs );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} )( jQuery, wp );
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Cap checker.
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
// Avoid direct calls to this file.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized_Exception class.
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
class File_Exception extends \Exception {
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* Transient Store.
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
// Avoid direct calls to this file.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient - based store.
|
||||
*/
|
||||
class Transient_Store extends Tus_Abstract_Cache {
|
||||
|
||||
/**
|
||||
* Get key.
|
||||
*
|
||||
* @param string $key The blog_id.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function cache_get( $key ) {
|
||||
$contents = get_transient( $key );
|
||||
return empty( $contents ) ? null : $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param array|mixed $value Even get the expired key.
|
||||
* @param bool $is_update Is this an update.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function cache_set( $key, $value, $is_update = false ) {
|
||||
if ( $is_update ) {
|
||||
delete_transient( $key );
|
||||
}
|
||||
return set_transient( $key, $value, $this->get_ttl() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function cache_delete( $key ) {
|
||||
return delete_transient( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache keys.
|
||||
*
|
||||
* @param string $prefix Prefix.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function cache_keys( $prefix ) {
|
||||
return get_transient( $prefix );
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* Main
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
// Avoid direct calls to this file.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract cache class.
|
||||
**/
|
||||
abstract class Tus_Abstract_Cache {
|
||||
|
||||
/**
|
||||
* Date format standard.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7231#section-7.1.1.1
|
||||
*
|
||||
* @var int
|
||||
**/
|
||||
const RFC_7231 = 'D, d M Y H:i:s \G\M\T';
|
||||
|
||||
/**
|
||||
* TTL in secs (default 1 day).
|
||||
*
|
||||
* @var int
|
||||
**/
|
||||
protected $ttl = 86400;
|
||||
|
||||
/** Prefix for cache keys.
|
||||
*
|
||||
* @var string
|
||||
**/
|
||||
protected $prefix = 'videopress_uploader_1_';
|
||||
|
||||
/**
|
||||
* Various blog_id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $blog_id = 'simple';
|
||||
|
||||
/**
|
||||
* Cache constructor.
|
||||
*
|
||||
* @param int|string $blog_id The blog_id.
|
||||
*/
|
||||
public function __construct( $blog_id ) {
|
||||
$this->blog_id = (string) $blog_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache Get key.
|
||||
*
|
||||
* @param string $key The blog_id.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
abstract protected function cache_get( $key );
|
||||
|
||||
/**
|
||||
* Set data to the given key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param bool $is_update Is update.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function cache_set( $key, $value, $is_update );
|
||||
|
||||
/**
|
||||
* Delete data associated with the key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function cache_delete( $key );
|
||||
|
||||
/**
|
||||
* Get cache keys.
|
||||
*
|
||||
* @param string $keys_prefix The key prefix.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function cache_keys( $keys_prefix );
|
||||
|
||||
/**
|
||||
* Build a cache key.
|
||||
*
|
||||
* @param string $key Key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function build_key( $key ) {
|
||||
$prefix = $this->get_prefix();
|
||||
|
||||
if ( ! str_starts_with( $key, $prefix ) ) {
|
||||
$key = $prefix . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key.
|
||||
*
|
||||
* @param string $key The blog_id.
|
||||
* @param bool $with_expired Even get the expired keys.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get( $key, $with_expired = false ) {
|
||||
$key = $this->build_key( $key );
|
||||
|
||||
$contents_str = $this->cache_get( $key );
|
||||
$contents = json_decode( $contents_str, true );
|
||||
|
||||
if ( ! $contents ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( $with_expired ) {
|
||||
return $contents;
|
||||
}
|
||||
|
||||
return $this->is_content_expired( $contents ) ? null : $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete( $key ) {
|
||||
$key = $this->build_key( $key );
|
||||
return $this->cache_delete( $key ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @param array|mixed $value Even get the expired key.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
$key = $this->build_key( $key );
|
||||
$cache_data = $this->get( $key );
|
||||
$contents = $cache_data ? $cache_data : array();
|
||||
|
||||
if ( \is_array( $value ) ) {
|
||||
$contents = array_merge( $contents, $value );
|
||||
} else {
|
||||
$contents[] = $value;
|
||||
}
|
||||
|
||||
$status = $this->cache_set( $key, \wp_json_encode( $contents, JSON_UNESCAPED_SLASHES ), ! empty( $cache_data ) );
|
||||
return false !== $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache keys.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function keys() {
|
||||
// TODO: This needs more thought.
|
||||
$keys_entry = $this->cache_keys( 'videopress_cache_keys_blog_' . $this->blog_id );
|
||||
if ( is_string( $keys_entry ) ) {
|
||||
return json_decode( $keys_entry );
|
||||
}
|
||||
return $keys_entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data associated with the keys.
|
||||
*
|
||||
* @param array $keys The keys to delete.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_all( $keys ) {
|
||||
$status = true;
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
$r = $this->delete( $this->build_key( $key ) );
|
||||
$status = $status && $r;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time to live.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_ttl() {
|
||||
return $this->ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time to live.
|
||||
*
|
||||
* @param int $secs The ttl.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function set_ttl( $secs ) {
|
||||
$this->ttl = $secs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache prefix.
|
||||
*
|
||||
* @param string $prefix The prefix.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function set_prefix( $prefix ) {
|
||||
$this->prefix = $prefix;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_prefix() {
|
||||
/**
|
||||
* Filters the cache prefix that will be used for VideoPress Uploader.
|
||||
*
|
||||
* @param string $cache_prefix The cache prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
return apply_filters( 'videopress_uploader_cache_prefix', $this->prefix );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log stuff
|
||||
*
|
||||
* @param string $what Stuff to log.
|
||||
*/
|
||||
protected function log( $what ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
do_action( 'videopress_uploader_log', 'info', get_class( $this ) . ' Cache Store: \n\n' . print_r( $what, true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if content has an expires_at entry and compares to time().
|
||||
*
|
||||
* @param array $contents The contents.
|
||||
*
|
||||
* @return bool
|
||||
**/
|
||||
protected function is_content_expired( $contents ) {
|
||||
if ( ! isset( $contents['expires_at'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expires_at = $contents['expires_at'];
|
||||
$date = Tus_Date_Utils::date_utc( $expires_at );
|
||||
|
||||
return absint( $date->getTimestamp() ) < time();
|
||||
}
|
||||
}
|
||||
+814
@@ -0,0 +1,814 @@
|
||||
<?php
|
||||
/**
|
||||
* Tus_Client
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
// phpcs:disable Generic.Commenting.DocComment.MissingShort
|
||||
// phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows
|
||||
// phpcs:disable Generic.Commenting.DocComment.MissingShort
|
||||
// phpcs:disable Squiz.Commenting.FunctionComment.MissingParamComment
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WP_Error;
|
||||
use WP_Http;
|
||||
|
||||
/**
|
||||
* Tus_Client
|
||||
*/
|
||||
class Tus_Client {
|
||||
|
||||
/** @const string Tus protocol version. */
|
||||
const TUS_PROTOCOL_VERSION = '1.0.0';
|
||||
|
||||
/** @const string Upload type partial. */
|
||||
const UPLOAD_TYPE_PARTIAL = 'partial';
|
||||
|
||||
/** @const string Upload type final. */
|
||||
const UPLOAD_TYPE_FINAL = 'final';
|
||||
|
||||
/** @const string Name separator for partial upload. */
|
||||
const PARTIAL_UPLOAD_NAME_SEPARATOR = '_';
|
||||
|
||||
/** @const string Upload type normal. */
|
||||
const UPLOAD_TYPE_NORMAL = 'normal';
|
||||
|
||||
/** @const string Header Content Type */
|
||||
const HEADER_CONTENT_TYPE = 'application/offset+octet-stream';
|
||||
|
||||
/** @const string Base API Uri */
|
||||
const BASE_API_URL = 'https://public-api.wordpress.com/rest/v1.1/video-uploads/%d';
|
||||
|
||||
/** @var Tus_Abstract_Cache */
|
||||
protected $cache;
|
||||
|
||||
/** @var string */
|
||||
protected $file_path;
|
||||
|
||||
/** @var int */
|
||||
protected $file_size = 0;
|
||||
|
||||
/** @var string */
|
||||
protected $file_name;
|
||||
|
||||
/** @var string */
|
||||
protected $key;
|
||||
|
||||
/** @var string */
|
||||
protected $url;
|
||||
|
||||
/** @var string */
|
||||
protected $checksum;
|
||||
|
||||
/** @var int */
|
||||
protected $partial_offset = -1;
|
||||
|
||||
/** @var bool */
|
||||
protected $partial = false;
|
||||
|
||||
/** @var string */
|
||||
protected $checksum_algorithm = 'sha256';
|
||||
|
||||
/** @var array */
|
||||
protected $metadata = array();
|
||||
|
||||
/** @var array */
|
||||
protected $headers = array();
|
||||
|
||||
/**
|
||||
* The details of the server response about the uploaded file
|
||||
* VideoPress mod: Create new attribute
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $uploaded_video_details = null;
|
||||
|
||||
/**
|
||||
* The API url composed by BASE_API_URL and the Blod ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $api_url = null;
|
||||
|
||||
/**
|
||||
* Tus_Client constructor.
|
||||
*
|
||||
* @param string $key The unique upload key identifier.
|
||||
* @param string $upload_token The upload token retrieved from the server.
|
||||
* @param int $blog_id The current Jetpack Blog ID.
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct( $key, $upload_token, $blog_id ) {
|
||||
|
||||
$this->key = $key;
|
||||
|
||||
$this->headers = array(
|
||||
'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
|
||||
'x-videopress-upload-token' => $upload_token,
|
||||
);
|
||||
|
||||
$this->api_url = sprintf( self::BASE_API_URL, (int) $blog_id );
|
||||
|
||||
$this->cache = new Transient_Store( $this->get_key() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache.
|
||||
*
|
||||
* @return Tus_Abstract_Cache
|
||||
*/
|
||||
public function get_cache() {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one attribute from the cache, if it exists.
|
||||
*
|
||||
* @param string $attribute The attribute name.
|
||||
* @return mixed The attribute value if found or null.
|
||||
*/
|
||||
public function get_cache_attribute( $attribute ) {
|
||||
$cache_values = $this->get_cache()->get( $this->get_key() );
|
||||
return ! empty( $cache_values[ $attribute ] ) ? $cache_values[ $attribute ] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the uploaded video details
|
||||
*
|
||||
* @param string $guid The guid of the created video.
|
||||
* @param string $media_id The ID of the attachment created.
|
||||
* @param string $upload_src The video URL.
|
||||
* @return void
|
||||
*/
|
||||
protected function set_uploaded_video_details( $guid, $media_id, $upload_src ) {
|
||||
$this->uploaded_video_details = compact( 'guid', 'media_id', 'upload_src' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the details of the uploaded video
|
||||
* VideoPress mod: Create new method
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_uploaded_video_details() {
|
||||
return $this->uploaded_video_details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file properties.
|
||||
*
|
||||
* @param string $file File path.
|
||||
* @param string|null $name File name.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Tus_Exception
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function file( $file, $name = null ) {
|
||||
if ( ! is_string( $file ) ) {
|
||||
throw new InvalidArgumentException( '$file needs to be a string' );
|
||||
}
|
||||
$this->file_path = $file;
|
||||
|
||||
if ( ! file_exists( $file ) || ! is_readable( $file ) ) {
|
||||
throw new Tus_Exception( 'Cannot read file: ' . $file );
|
||||
}
|
||||
|
||||
$this->file_name = ! empty( $name ) ? basename( $this->file_path ) : '';
|
||||
$this->file_size = filesize( $file );
|
||||
|
||||
$this->add_metadata( 'filename', $this->file_name );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file path.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_file_path() {
|
||||
return $this->file_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file name.
|
||||
*
|
||||
* @param string $name The file name.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function set_file_name( $name ) {
|
||||
if ( ! is_string( $name ) ) {
|
||||
throw new InvalidArgumentException( '$name needs to be a string' );
|
||||
}
|
||||
$this->add_metadata( 'filename', $this->file_name = $name );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file name.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_file_name() {
|
||||
return $this->file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_file_size() {
|
||||
return $this->file_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set checksum.
|
||||
*
|
||||
* @param string $checksum
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function set_checksum( $checksum ) {
|
||||
if ( ! is_string( $checksum ) ) {
|
||||
throw new InvalidArgumentException( '$checksum needs to be a string' );
|
||||
}
|
||||
$this->checksum = $checksum;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checksum.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_checksum() {
|
||||
if ( empty( $this->checksum ) ) {
|
||||
$this->set_checksum( hash_file( $this->get_checksum_algorithm(), $this->get_file_path() ) );
|
||||
}
|
||||
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function add_metadata( $key, $value ) {
|
||||
if ( ! is_string( $key ) || ! is_string( $value ) ) {
|
||||
throw new InvalidArgumentException( '$key and $value need to be strings' );
|
||||
}
|
||||
$this->metadata[ $key ] = base64_encode( $value ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove metadata.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function remove_metadata( $key ) {
|
||||
if ( ! is_string( $key ) ) {
|
||||
throw new InvalidArgumentException( '$key needs to be a string' );
|
||||
}
|
||||
unset( $this->metadata[ $key ] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metadata.
|
||||
*
|
||||
* @param array $items
|
||||
*
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function set_metadata( array $items ) {
|
||||
$items = array_map( 'base64_encode', $items );
|
||||
|
||||
$this->metadata = $items;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_metadata() {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for Upload-Metadata header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_upload_metadata_header() {
|
||||
$metadata = array();
|
||||
|
||||
foreach ( $this->get_metadata() as $key => $value ) {
|
||||
$metadata[] = "{$key} {$value}";
|
||||
}
|
||||
|
||||
return implode( ',', $metadata );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_key() {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url.
|
||||
*
|
||||
* @throws File_Exception
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_url() {
|
||||
$this->url = $this->get_cache_attribute( 'location' );
|
||||
|
||||
if ( ! $this->url ) {
|
||||
throw new File_Exception( 'File not found.' );
|
||||
}
|
||||
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set checksum algorithm.
|
||||
*
|
||||
* @param string $algorithm
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return Tus_Client
|
||||
*/
|
||||
public function set_checksum_algorithm( $algorithm ) {
|
||||
if ( ! is_string( $algorithm ) ) {
|
||||
throw new InvalidArgumentException( '$algorithm needs to be a string' );
|
||||
}
|
||||
$this->checksum_algorithm = $algorithm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checksum algorithm.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_checksum_algorithm() {
|
||||
return $this->checksum_algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current upload is expired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_expired() {
|
||||
$expires_at = $this->get_cache_attribute( 'expires_at' );
|
||||
|
||||
return empty( $expires_at ) || time() > strtotime( $expires_at );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a partial upload request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_partial() {
|
||||
return $this->partial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partial offset.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_partial_offset() {
|
||||
return $this->partial_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset and force this to be a partial upload request.
|
||||
*
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return self
|
||||
*/
|
||||
public function seek( $offset ) {
|
||||
if ( ! is_int( $offset ) ) {
|
||||
throw new InvalidArgumentException( '$offset needs to be an integer' );
|
||||
}
|
||||
$this->partial_offset = $offset;
|
||||
|
||||
$this->partial();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file.
|
||||
*
|
||||
* @param int $bytes Bytes to upload.
|
||||
*
|
||||
* @throws Tus_Exception
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function upload( $bytes = -1 ) {
|
||||
if ( ! is_int( $bytes ) ) {
|
||||
throw new InvalidArgumentException( '$bytes needs to be an integer' );
|
||||
}
|
||||
$bytes = $bytes < 0 ? $this->get_file_size() : $bytes;
|
||||
$partial_offset = $this->partial_offset < 0 ? 0 : $this->partial_offset;
|
||||
|
||||
$offset = $this->get_offset();
|
||||
if ( is_wp_error( $offset ) ) {
|
||||
throw new Tus_Exception( "Couldn't connect to server." );
|
||||
}
|
||||
|
||||
if ( false === $offset ) {
|
||||
$this->url = $this->create( $this->get_key() );
|
||||
$offset = $partial_offset;
|
||||
}
|
||||
|
||||
// Verify that upload is not yet expired.
|
||||
if ( $this->is_expired() ) {
|
||||
throw new Tus_Exception( 'Upload expired.' );
|
||||
}
|
||||
|
||||
// Now, resume upload with PATCH request.
|
||||
return $this->send_patch_request( $bytes, $offset );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create resource with POST request.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function create( $key ) {
|
||||
if ( ! is_string( $key ) ) {
|
||||
throw new InvalidArgumentException( '$key needs to be a string' );
|
||||
}
|
||||
return $this->create_with_upload( $key, 0 )['location'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create resource with POST request and upload data using the creation-with-upload extension.
|
||||
*
|
||||
* @see https://tus.io/protocols/resumable-upload.html#creation-with-upload
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $bytes -1 => all data; 0 => no data.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Tus_Exception
|
||||
*
|
||||
* @return array [
|
||||
* 'location' => string,
|
||||
* 'offset' => int
|
||||
* ]
|
||||
*/
|
||||
public function create_with_upload( $key, $bytes = -1 ) {
|
||||
if ( ! is_string( $key ) ) {
|
||||
throw new InvalidArgumentException( '$key needs to be a string' );
|
||||
}
|
||||
$bytes = $bytes < 0 ? $this->file_size : $bytes;
|
||||
|
||||
$headers = $this->headers + array(
|
||||
'Upload-Length' => $this->file_size,
|
||||
'Upload-Key' => $key,
|
||||
'Upload-Checksum' => $this->get_upload_checksum_header(),
|
||||
'Upload-Metadata' => $this->get_upload_metadata_header(),
|
||||
);
|
||||
|
||||
$data = '';
|
||||
if ( $bytes > 0 ) {
|
||||
$data = $this->get_data( 0, $bytes );
|
||||
|
||||
$headers += array(
|
||||
'Content-Type' => self::HEADER_CONTENT_TYPE,
|
||||
'Content-Length' => \strlen( $data ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->is_partial() ) {
|
||||
$headers += array( 'Upload-Concat' => 'partial' );
|
||||
}
|
||||
|
||||
$response = $this->do_post_request(
|
||||
$this->api_url,
|
||||
array(
|
||||
'body' => $data,
|
||||
'headers' => $headers,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new Tus_Exception( 'Error reaching the server.' );
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( WP_Http::CREATED !== $status_code ) {
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
$error_message = __( 'Unable to create resource.', 'jetpack-videopress-pkg' );
|
||||
if ( ! empty( $body->message ) ) {
|
||||
$error_message = $body->message;
|
||||
if ( ! empty( $body->error ) ) {
|
||||
$error_message = $body->error . ': ' . $error_message;
|
||||
}
|
||||
}
|
||||
// server can respond in a few different ways.
|
||||
if ( isset( $body->success ) && false === $body->success ) {
|
||||
if ( ! empty( $body->data ) && ! empty( $body->data->message ) ) {
|
||||
$error_message = $body->data->message;
|
||||
}
|
||||
}
|
||||
throw new Tus_Exception( $error_message, $status_code );
|
||||
}
|
||||
|
||||
$upload_offset = $bytes > 0 ? wp_remote_retrieve_header( $response, 'upload-offset' ) : 0;
|
||||
$upload_location = wp_remote_retrieve_header( $response, 'location' );
|
||||
|
||||
$cache = $this->get_cache();
|
||||
|
||||
$cache->set(
|
||||
$this->get_key(),
|
||||
array(
|
||||
'location' => $upload_location,
|
||||
'expires_at' => gmdate( $cache::RFC_7231, time() + $cache->get_ttl() ),
|
||||
'token_for_key' => wp_remote_retrieve_header( $response, 'x-videopress-upload-key-token' ),
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'location' => $upload_location,
|
||||
'offset' => $upload_offset,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as partial request.
|
||||
*
|
||||
* @param bool $state
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
protected function partial( $state = true ) {
|
||||
if ( ! is_bool( $state ) ) {
|
||||
throw new InvalidArgumentException( '$state needs to be a boolean' );
|
||||
}
|
||||
$this->partial = $state;
|
||||
|
||||
if ( ! $this->partial ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $this->get_key();
|
||||
|
||||
if ( str_contains( $key, self::PARTIAL_UPLOAD_NAME_SEPARATOR ) ) {
|
||||
list($key, /* $partialKey */) = explode( self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key );
|
||||
}
|
||||
|
||||
$this->key = $key . self::PARTIAL_UPLOAD_NAME_SEPARATOR . wp_generate_uuid4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HEAD request and retrieves offset.
|
||||
*
|
||||
* @return bool|int|WP_Error integer with the offset uploaded if file exists. False if file does not exist. WP_Error on connection error.
|
||||
*/
|
||||
public function get_offset() {
|
||||
$headers = $this->headers + array(
|
||||
'X-HTTP-Method-Override' => 'HEAD',
|
||||
);
|
||||
|
||||
try {
|
||||
$response = $this->do_get_request(
|
||||
$this->get_url(),
|
||||
array( 'headers' => $headers )
|
||||
);
|
||||
} catch ( File_Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( WP_Http::OK !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) wp_remote_retrieve_header( $response, 'upload-offset' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send PATCH request.
|
||||
*
|
||||
* @param int $bytes
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function send_patch_request( $bytes, $offset ) {
|
||||
if ( ! is_int( $bytes ) || ! is_int( $offset ) ) {
|
||||
throw new InvalidArgumentException( '$bytes and $offset need to be integers' );
|
||||
}
|
||||
$data = $this->get_data( $offset, $bytes );
|
||||
$headers = $this->headers + array(
|
||||
'Content-Type' => self::HEADER_CONTENT_TYPE,
|
||||
'Content-Length' => \strlen( $data ),
|
||||
'Upload-Checksum' => $this->get_upload_checksum_header(),
|
||||
'X-HTTP-Method-Override' => 'PATCH',
|
||||
|
||||
);
|
||||
|
||||
$token = $this->get_cache_attribute( 'token_for_key' );
|
||||
$headers['x-videopress-upload-token'] = $token;
|
||||
|
||||
if ( $this->is_partial() ) {
|
||||
$headers += array( 'Upload-Concat' => self::UPLOAD_TYPE_PARTIAL );
|
||||
} else {
|
||||
$headers += array( 'Upload-Offset' => $offset );
|
||||
}
|
||||
|
||||
$response = $this->do_post_request(
|
||||
$this->get_url(),
|
||||
array(
|
||||
'body' => $data,
|
||||
'headers' => $headers,
|
||||
)
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( WP_Http::NO_CONTENT !== $response_code ) {
|
||||
throw $this->handle_patch_exception( $response );
|
||||
}
|
||||
|
||||
$guid = wp_remote_retrieve_header( $response, 'x-videopress-upload-guid' );
|
||||
$media_id = (int) wp_remote_retrieve_header( $response, 'x-videopress-upload-media-id' );
|
||||
$upload_src = wp_remote_retrieve_header( $response, 'x-videopress-upload-src-url' );
|
||||
|
||||
if ( $guid && $media_id && $upload_src ) {
|
||||
$this->set_uploaded_video_details( $guid, $media_id, $upload_src );
|
||||
}
|
||||
|
||||
return (int) wp_remote_retrieve_header( $response, 'upload-offset' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle client exception during patch request.
|
||||
*
|
||||
* @param array $response The response from the PATCH request.
|
||||
*
|
||||
* @return \Exception
|
||||
*/
|
||||
protected function handle_patch_exception( $response ) {
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new Tus_Exception( $response->get_error_message() );
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( WP_Http::REQUESTED_RANGE_NOT_SATISFIABLE === $response_code ) {
|
||||
return new Tus_Exception( 'The uploaded file is corrupt.' );
|
||||
}
|
||||
|
||||
if ( WP_Http::HTTP_CONTINUE === $response_code ) {
|
||||
return new Tus_Exception( 'Connection aborted by user.' );
|
||||
}
|
||||
|
||||
if ( WP_Http::UNSUPPORTED_MEDIA_TYPE === $response_code ) {
|
||||
return new Tus_Exception( 'Unsupported media types.' );
|
||||
}
|
||||
|
||||
return new Tus_Exception( (string) wp_remote_retrieve_body( $response ), $response_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get X bytes of data from file.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $bytes
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_data( $offset, $bytes ) {
|
||||
if ( ! is_int( $bytes ) || ! is_int( $offset ) ) {
|
||||
throw new InvalidArgumentException( '$bytes and $offset need to be integers' );
|
||||
}
|
||||
$file = new Tus_File();
|
||||
$handle = $file->open( $this->get_file_path(), $file::READ_BINARY );
|
||||
|
||||
$file->seek( $handle, $offset );
|
||||
|
||||
$data = $file->read( $handle, $bytes );
|
||||
|
||||
$file->close( $handle );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload checksum header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_upload_checksum_header() {
|
||||
return $this->get_checksum_algorithm() . ' ' . base64_encode( $this->get_checksum() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
}
|
||||
|
||||
/**
|
||||
* Do HTTP request
|
||||
*
|
||||
* @param string $url The URL to make the request to.
|
||||
* @param array $args Request arguments.
|
||||
* @return array|WP_Error WordPress Http response
|
||||
*/
|
||||
protected function do_request( $url, $args ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
array(
|
||||
'timeout' => 25,
|
||||
)
|
||||
);
|
||||
return wp_remote_request( $url, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a GET HTTP request
|
||||
*
|
||||
* @param string $url The URL to make the request to.
|
||||
* @param array $args Request arguments.
|
||||
* @return array|WP_Error WordPress Http response
|
||||
*/
|
||||
protected function do_get_request( $url, $args ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
array(
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
return $this->do_request( $url, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a POST HTTP request
|
||||
*
|
||||
* @param string $url The URL to make the request to.
|
||||
* @param array $args Request arguments.
|
||||
* @return array|WP_Error WordPress Http response
|
||||
*/
|
||||
protected function do_post_request( $url, $args ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
array(
|
||||
'method' => 'POST',
|
||||
)
|
||||
);
|
||||
return $this->do_request( $url, $args );
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Tus_Date Utils.
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
// Avoid direct calls to this file.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Tus_Date_Utils
|
||||
*/
|
||||
class Tus_Date_Utils {
|
||||
/**
|
||||
* Returns a UTC date.
|
||||
*
|
||||
* @param null|int|string $s Thing to turn to date utc.
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
* @throws \Exception If the thing provided does not make sense.
|
||||
*/
|
||||
public static function date_utc( $s = null ) {
|
||||
return new \DateTimeImmutable( $s, new \DateTimeZone( 'UTC' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds seconds to a date.
|
||||
*
|
||||
* @param \DateTimeImmutable $date The date.
|
||||
* @param int $seconds The seconds.
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
* @throws \Exception If invalid interval.
|
||||
*/
|
||||
public static function add_seconds( \DateTimeImmutable $date, $seconds ) {
|
||||
return $date->add( new \DateInterval( 'PT' . absint( $seconds ) . 'S' ) );
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* Tus Exception
|
||||
*
|
||||
* @package jetpack-videopress
|
||||
*/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
/**
|
||||
* Tus Exception
|
||||
*/
|
||||
class Tus_Exception extends \Exception {
|
||||
|
||||
}
|
||||
+684
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
/**
|
||||
* Main
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
**/
|
||||
|
||||
namespace VideoPressUploader;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
// Avoid direct calls to this file.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Tus_File.
|
||||
*
|
||||
* @package VideoPressUploader
|
||||
*/
|
||||
class Tus_File {
|
||||
const CHUNK_SIZE = 8192; // 8 kilobytes.
|
||||
const INPUT_STREAM = 'php://input';
|
||||
const READ_BINARY = 'rb';
|
||||
const APPEND_BINARY = 'ab';
|
||||
|
||||
/**
|
||||
* The input stream.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $input_stream = self::INPUT_STREAM;
|
||||
|
||||
/**
|
||||
* The key.
|
||||
*
|
||||
* @var string The key.
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* The file checksum.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $checksum;
|
||||
|
||||
/**
|
||||
* The file name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The cache we are using.
|
||||
*
|
||||
* @var Tus_Abstract_Cache
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The current file offset.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $offset;
|
||||
|
||||
/**
|
||||
* The location.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $location;
|
||||
|
||||
/**
|
||||
* The file path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $file_path;
|
||||
|
||||
/**
|
||||
* The file size.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $file_size;
|
||||
|
||||
/**
|
||||
* The upload metadata.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $upload_metadata = array();
|
||||
|
||||
/**
|
||||
* File constructor.
|
||||
*
|
||||
* @param string|null $name Name.
|
||||
* @param Tus_Abstract_Cache|null $cache Cache.
|
||||
*/
|
||||
public function __construct( $name = null, $cache = null ) {
|
||||
$this->name = $name;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an integer if it's castable. Otherwise it throws
|
||||
*
|
||||
* @param string|int $number Number to cast.
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return int
|
||||
*/
|
||||
public function ensure_integer( $number ) {
|
||||
if ( ! is_numeric( $number ) ) {
|
||||
throw new InvalidArgumentException( 'argument needs to be an integer. Check stacktrace' );
|
||||
}
|
||||
return (int) $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file meta.
|
||||
*
|
||||
* @param int $offset Offset.
|
||||
* @param int $file_size File size.
|
||||
* @param string $file_path File path.
|
||||
* @param string $location Location.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return $this
|
||||
*/
|
||||
public function set_meta( $offset, $file_size, $file_path, $location = null ) {
|
||||
$offset = $this->ensure_integer( $offset );
|
||||
$file_size = $this->ensure_integer( $file_size );
|
||||
|
||||
if ( ! is_string( $file_path ) ) {
|
||||
throw new InvalidArgumentException( '$file_path needs to be a string' );
|
||||
}
|
||||
|
||||
$this->offset = absint( $offset );
|
||||
$this->file_size = absint( $file_size );
|
||||
$this->file_path = $file_path;
|
||||
$this->location = $location;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name Name.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function set_name( $name ) {
|
||||
if ( ! is_string( $name ) ) {
|
||||
throw new InvalidArgumentException( '$name needs to be a string' );
|
||||
}
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file size.
|
||||
*
|
||||
* @param int $size The size.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return Tus_File
|
||||
*/
|
||||
public function set_file_size( $size ) {
|
||||
$size = $this->ensure_integer( $size );
|
||||
$this->file_size = $size;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_file_size() {
|
||||
return $this->file_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set key.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return Tus_File
|
||||
*/
|
||||
public function set_key( $key ) {
|
||||
if ( ! is_string( $key ) ) {
|
||||
throw new InvalidArgumentException( '$key needs to be a string' );
|
||||
}
|
||||
$this->key = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_key() {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set checksum.
|
||||
*
|
||||
* @param string $checksum The checksum.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return Tus_File
|
||||
*/
|
||||
public function set_checksum( $checksum ) {
|
||||
if ( ! is_string( $checksum ) ) {
|
||||
throw new InvalidArgumentException( '$checksum needs to be a string' );
|
||||
}
|
||||
$this->checksum = $checksum;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checksum.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_checksum() {
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset.
|
||||
*
|
||||
* @param int $offset The offset.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return self
|
||||
*/
|
||||
public function set_offset( $offset ) {
|
||||
$offset = $this->ensure_integer( $offset );
|
||||
$this->offset = absint( $offset );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_offset() {
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set location.
|
||||
*
|
||||
* @param string $location The location.
|
||||
*
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
* @return self
|
||||
*/
|
||||
public function set_location( $location ) {
|
||||
if ( ! is_string( $location ) ) {
|
||||
throw new InvalidArgumentException( '$location needs to be a string' );
|
||||
}
|
||||
$this->location = $location;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_location() {
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set absolute file location.
|
||||
*
|
||||
* @param string $path The path.
|
||||
*
|
||||
* @return Tus_File
|
||||
*/
|
||||
public function set_file_path( $path ) {
|
||||
$this->file_path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_path() {
|
||||
return $this->file_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the upload meta.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
*
|
||||
* @return Tus_File
|
||||
*/
|
||||
public function set_upload_metadata( array $metadata ) {
|
||||
$this->upload_metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get input stream.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_input_stream() {
|
||||
return self::$input_stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set input stream. Useful for testing.
|
||||
*
|
||||
* @param string $stream The stream.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_input_stream( $stream ) {
|
||||
self::$input_stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file meta.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception If date fails.
|
||||
*/
|
||||
public function details() {
|
||||
$now = Tus_Date_Utils::date_utc();
|
||||
$ttl = $this->cache->get_ttl();
|
||||
|
||||
return array(
|
||||
'name' => $this->name,
|
||||
'size' => $this->file_size,
|
||||
'offset' => $this->offset,
|
||||
'checksum' => $this->checksum,
|
||||
'location' => $this->location,
|
||||
'file_path' => $this->file_path,
|
||||
'metadata' => $this->upload_metadata,
|
||||
'created_at' => $now->format( Tus_Abstract_Cache::RFC_7231 ),
|
||||
'expires_at' => Tus_Date_Utils::add_seconds( $now, $ttl )->format( Tus_Abstract_Cache::RFC_7231 ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to server.
|
||||
*
|
||||
* @param int $total_bytes The total bytes of the file.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Out_Of_Range_Exception Various exceptions.
|
||||
* @throws \Connection_Exception Various exceptions.
|
||||
* @throws File_Exception Various exceptions.
|
||||
*/
|
||||
public function upload( $total_bytes ) {
|
||||
if ( $this->offset === $total_bytes ) {
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
try {
|
||||
$input = $this->open( $this->get_input_stream(), self::READ_BINARY );
|
||||
$output = $this->open( $this->get_file_path(), self::APPEND_BINARY );
|
||||
$key = $this->get_key();
|
||||
} catch ( File_Exception $fe ) {
|
||||
Logger::log( 'error', $fe );
|
||||
throw new File_Exception( 'Upload failed.' );
|
||||
}
|
||||
|
||||
try {
|
||||
$this->seek( $output, $this->offset );
|
||||
|
||||
while ( ! feof( $input ) ) {
|
||||
if ( CONNECTION_NORMAL !== connection_status() ) {
|
||||
throw new \Connection_Exception( 'Connection aborted by user.' );
|
||||
}
|
||||
|
||||
$data = $this->read( $input, self::CHUNK_SIZE );
|
||||
$bytes = $this->write( $output, $data, self::CHUNK_SIZE );
|
||||
|
||||
$this->offset += $bytes;
|
||||
|
||||
if ( $this->offset > $total_bytes ) {
|
||||
throw new \Out_Of_Range_Exception( 'The uploaded file is corrupt.' );
|
||||
}
|
||||
|
||||
if ( $this->offset === $total_bytes ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->close( $input );
|
||||
$this->close( $output );
|
||||
|
||||
try {
|
||||
$this->cache->set( $key, array( 'offset' => $this->offset ) );
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::log( 'error', $e );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file in given mode.
|
||||
*
|
||||
* @param string $file_path The file path.
|
||||
* @param string $mode The mode.
|
||||
*
|
||||
* @return resource
|
||||
* @throws File_Exception Exc.
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
*/
|
||||
public function open( $file_path, $mode ) {
|
||||
if ( ! is_string( $file_path ) ) {
|
||||
throw new InvalidArgumentException( '$file_path needs to be a string' );
|
||||
}
|
||||
if ( ! is_string( $mode ) ) {
|
||||
throw new InvalidArgumentException( '$mode needs to be a string' );
|
||||
}
|
||||
$this->exists( $file_path, $mode );
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
$ptr = @fopen( $file_path, $mode );
|
||||
|
||||
if ( false === $ptr ) {
|
||||
Logger::log( 'error', "Unable to open file at $file_path." );
|
||||
throw new File_Exception( 'Unable to open file.' );
|
||||
}
|
||||
|
||||
return $ptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file to read exists.
|
||||
*
|
||||
* @param string $file_path The file path.
|
||||
* @param string $mode The mode.
|
||||
*
|
||||
* @return bool
|
||||
* @throws File_Exception File.
|
||||
* @throws InvalidArgumentException If argument is invalid.
|
||||
*/
|
||||
public function exists( $file_path, $mode = self::READ_BINARY ) {
|
||||
if ( ! is_string( $file_path ) ) {
|
||||
throw new InvalidArgumentException( '$file_path needs to be a string' );
|
||||
}
|
||||
if ( self::INPUT_STREAM === $file_path ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( self::READ_BINARY === $mode && ! file_exists( $file_path ) ) {
|
||||
throw new File_Exception( 'File not found.' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file pointer to given offset using fseek.
|
||||
*
|
||||
* @param resource $handle The handle.
|
||||
* @param int $offset The offset.
|
||||
* @param int $whence The whence.
|
||||
*
|
||||
* @throws File_Exception Exc.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function seek( $handle, $offset, $whence = SEEK_SET ) {
|
||||
$offset = $this->ensure_integer( $offset );
|
||||
$position = fseek( $handle, $offset, $whence );
|
||||
|
||||
if ( -1 === $position ) {
|
||||
throw new File_Exception( 'Cannot move pointer to desired position.' );
|
||||
}
|
||||
|
||||
return $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from file.
|
||||
*
|
||||
* @param resource $handle The handle.
|
||||
* @param int $chunk_size Chunk size.
|
||||
*
|
||||
* @return string
|
||||
* @throws File_Exception If no data is read.
|
||||
*/
|
||||
public function read( $handle, $chunk_size ) {
|
||||
$chunk_size = $this->ensure_integer( $chunk_size );
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
|
||||
$data = fread( $handle, $chunk_size );
|
||||
|
||||
if ( false === $data ) {
|
||||
throw new File_Exception( 'Cannot read file.' );
|
||||
}
|
||||
|
||||
return (string) $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to file.
|
||||
*
|
||||
* @param resource $handle The file handle.
|
||||
* @param string $data The data to write.
|
||||
* @param int|null $length Possibly the length of the data.
|
||||
*
|
||||
* @throws File_Exception When can't write.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function write( $handle, $data, $length = null ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
|
||||
$bytes_written = \is_numeric( $length ) ? fwrite( $handle, $data, intval( $length ) ) : fwrite( $handle, $data );
|
||||
|
||||
if ( false === $bytes_written ) {
|
||||
throw new File_Exception( 'Cannot write to a file.' );
|
||||
}
|
||||
|
||||
return $bytes_written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge 2 or more files.
|
||||
*
|
||||
* @param array $files File data with meta info.
|
||||
*
|
||||
* @return int
|
||||
* @throws File_Exception When the file to be merged is not found.
|
||||
*/
|
||||
public function merge( array $files ) {
|
||||
$destination = $this->get_file_path();
|
||||
$first_file = array_shift( $files );
|
||||
|
||||
// First partial file can directly be copied.
|
||||
$this->copy( $first_file['file_path'], $destination );
|
||||
|
||||
$this->offset = $first_file['offset'];
|
||||
$this->file_size = filesize( $first_file['file_path'] );
|
||||
|
||||
$handle = $this->open( $destination, self::APPEND_BINARY );
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
if ( ! file_exists( $file['file_path'] ) ) {
|
||||
throw new File_Exception( 'File to be merged not found.' );
|
||||
}
|
||||
|
||||
$this->file_size += $this->write( $handle, $this->get_wp_filesystem()->get_contents( $file['file_path'] ) );
|
||||
|
||||
$this->offset += $file['offset'];
|
||||
}
|
||||
|
||||
$this->close( $handle );
|
||||
|
||||
return $this->file_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy file from source to destination.
|
||||
*
|
||||
* @param string $source The source.
|
||||
* @param string $destination The destination.
|
||||
*
|
||||
* @return bool
|
||||
* @throws File_Exception If copy fails.
|
||||
*/
|
||||
public function copy( $source, $destination ) {
|
||||
$status = copy( $source, $destination );
|
||||
|
||||
if ( ! $status ) {
|
||||
Logger::log( 'error', sprintf( 'Cannot copy source (%s) to destination (%s).', $source, $destination ) );
|
||||
throw new File_Exception( 'Cannot copy source file to destination file.' );
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file and/or folder.
|
||||
*
|
||||
* @param array $files The files.
|
||||
* @param bool $folder The folder.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( array $files, $folder = false ) {
|
||||
$status = $this->delete_files( $files );
|
||||
|
||||
if ( $status && $folder ) {
|
||||
return $this->get_wp_filesystem()->rmdir( \dirname( current( $files ) ) );
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple files.
|
||||
*
|
||||
* @param array $files The files.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_files( array $files ) {
|
||||
if ( empty( $files ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = true;
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
if ( $this->get_wp_filesystem()->exists( $file ) ) {
|
||||
$r = $this->get_wp_filesystem()->delete( $file );
|
||||
$status = $status && $r;
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close file.
|
||||
*
|
||||
* @param mixed $handle The handle.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function close( $handle ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
||||
return fclose( $handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wp filesystem.
|
||||
*
|
||||
* @return \WP_Filesystem_Base|null
|
||||
*/
|
||||
private function get_wp_filesystem() {
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( ! isset( $wp_filesystem ) ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/file.php';
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
return $wp_filesystem;
|
||||
}
|
||||
}
|
||||
+801
@@ -0,0 +1,801 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
defined( 'VIDEOPRESS_MIN_WIDTH' ) || define( 'VIDEOPRESS_MIN_WIDTH', 60 );
|
||||
defined( 'VIDEOPRESS_DEFAULT_WIDTH' ) || define( 'VIDEOPRESS_DEFAULT_WIDTH', 640 );
|
||||
|
||||
// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
|
||||
|
||||
/**
|
||||
* VideoPress Privacy constants.
|
||||
*/
|
||||
abstract class VIDEOPRESS_PRIVACY {
|
||||
const IS_PUBLIC = 0;
|
||||
const IS_PRIVATE = 1;
|
||||
const SITE_DEFAULT = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user-supplied guid values against expected inputs
|
||||
*
|
||||
* @since 1.1
|
||||
* @param string $guid video identifier.
|
||||
* @return bool true if passes validation test
|
||||
*/
|
||||
function videopress_is_valid_guid( $guid ) {
|
||||
if ( ! empty( $guid ) && is_string( $guid ) && strlen( $guid ) === 8 && ctype_alnum( $guid ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates user-supplied video preload setting.
|
||||
*
|
||||
* @param mixed $value the preload value to validate.
|
||||
* @return bool
|
||||
*/
|
||||
function videopress_is_valid_preload( $value ) {
|
||||
return in_array( strtolower( $value ), array( 'auto', 'metadata', 'none' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details about a specific video by GUID:
|
||||
*
|
||||
* @param string $guid Video GUID.
|
||||
* @return object
|
||||
*/
|
||||
function videopress_get_video_details( $guid ) {
|
||||
if ( ! videopress_is_valid_guid( $guid ) ) {
|
||||
return new WP_Error( 'bad-guid-format', __( 'Invalid Video GUID!', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
|
||||
$version = '1.1';
|
||||
$endpoint = sprintf( '/videos/%1$s', $guid );
|
||||
$query_url = sprintf(
|
||||
'https://public-api.wordpress.com/rest/v%1$s%2$s',
|
||||
$version,
|
||||
$endpoint
|
||||
);
|
||||
|
||||
// Look for data in our transient. If nothing, let's make a new query.
|
||||
$data_from_cache = get_transient( 'jetpack_videopress_' . $guid );
|
||||
if ( false === $data_from_cache ) {
|
||||
$response = wp_remote_get( esc_url_raw( $query_url ) );
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
// Cache the response for an hour.
|
||||
set_transient( 'jetpack_videopress_' . $guid, $data, HOUR_IN_SECONDS );
|
||||
} else {
|
||||
$data = $data_from_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow functions to modify fetched video details.
|
||||
*
|
||||
* This filter allows third-party code to modify the return data
|
||||
* about a given video. It may involve swapping some data out or
|
||||
* adding new parameters.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param object $data The data returned by the WPCOM API. See: https://developer.wordpress.com/docs/api/1.1/get/videos/%24guid/
|
||||
* @param string $guid The GUID of the VideoPress video in question.
|
||||
*/
|
||||
return apply_filters( 'videopress_get_video_details', $data, $guid );
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `media_sideload_image` -- but returns an ID.
|
||||
*
|
||||
* @param string $url Image URL.
|
||||
* @param int $attachment_id Post ID.
|
||||
*
|
||||
* @return int|mixed|object|WP_Error
|
||||
*/
|
||||
function videopress_download_poster_image( $url, $attachment_id ) {
|
||||
// Set variables for storage, fix file filename for query strings.
|
||||
preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $url, $matches );
|
||||
if ( ! $matches ) {
|
||||
return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL', 'jetpack-videopress-pkg' ) );
|
||||
}
|
||||
|
||||
$file_array = array();
|
||||
$file_array['name'] = basename( $matches[0] );
|
||||
$file_array['tmp_name'] = download_url( $url );
|
||||
|
||||
// If error storing temporarily, return the error.
|
||||
if ( is_wp_error( $file_array['tmp_name'] ) ) {
|
||||
return $file_array['tmp_name'];
|
||||
}
|
||||
|
||||
// Do the validation and storage stuff.
|
||||
$thumbnail_id = media_handle_sideload( $file_array, $attachment_id, null );
|
||||
|
||||
// Flag it as poster image, so we can exclude it from display.
|
||||
update_post_meta( $thumbnail_id, 'videopress_poster_image', 1 );
|
||||
|
||||
return $thumbnail_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a local media library item of a remote VideoPress video.
|
||||
*
|
||||
* @param string $guid Video GUID.
|
||||
* @param int $parent_id Parent post ID.
|
||||
*
|
||||
* @return int|object
|
||||
*/
|
||||
function create_local_media_library_for_videopress_guid( $guid, $parent_id = 0 ) {
|
||||
$vp_data = videopress_get_video_details( $guid );
|
||||
if ( ! $vp_data || is_wp_error( $vp_data ) ) {
|
||||
return $vp_data;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'post_date' => $vp_data->upload_date,
|
||||
'post_title' => wp_kses( $vp_data->title, array() ),
|
||||
'post_content' => wp_kses( $vp_data->description, array() ),
|
||||
'post_mime_type' => 'video/videopress',
|
||||
'guid' => sprintf( 'https://videopress.com/v/%s', $guid ),
|
||||
);
|
||||
|
||||
$attachment_id = wp_insert_attachment( $args, null, $parent_id );
|
||||
|
||||
if ( ! is_wp_error( $attachment_id ) ) {
|
||||
update_post_meta( $attachment_id, 'videopress_guid', $guid );
|
||||
wp_update_attachment_metadata(
|
||||
$attachment_id,
|
||||
array(
|
||||
'width' => $vp_data->width,
|
||||
'height' => $vp_data->height,
|
||||
)
|
||||
);
|
||||
|
||||
$thumbnail_id = videopress_download_poster_image( $vp_data->poster, $attachment_id );
|
||||
update_post_meta( $attachment_id, '_thumbnail_id', $thumbnail_id );
|
||||
}
|
||||
|
||||
return $attachment_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that will look for VideoPress media items that are more than 30 minutes old,
|
||||
* that have not had anything attached to them by a wpcom upload and deletes the ghost
|
||||
* attachment.
|
||||
*
|
||||
* These happen primarily because of failed upload attempts.
|
||||
*
|
||||
* @return int The number of items that were cleaned up.
|
||||
*/
|
||||
function videopress_cleanup_media_library() {
|
||||
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable -- Function is disabled currently.
|
||||
// Disable this job for now.
|
||||
return 0;
|
||||
$query_args = array(
|
||||
'post_type' => 'attachment',
|
||||
'post_status' => 'inherit',
|
||||
'post_mime_type' => 'video/videopress',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'videopress_status',
|
||||
'value' => 'new',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Query( $query_args );
|
||||
|
||||
$cleaned = 0;
|
||||
|
||||
$now = current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- Probably should refactor, but this function is disabled.
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
foreach ( $query->posts as $post ) {
|
||||
$post_time = strtotime( $post->post_date_gmt );
|
||||
|
||||
// If the post is older than 30 minutes, it is safe to delete it.
|
||||
if ( $now - $post_time > MINUTE_IN_SECONDS * 30 ) {
|
||||
// Force delete the attachment, because we don't want it appearing in the trash.
|
||||
wp_delete_attachment( $post->ID, true );
|
||||
|
||||
++$cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $cleaned;
|
||||
// phpcs:enable Squiz.PHP.NonExecutableCode.Unreachable
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an absolute URI for a given filename and guid on the CDN.
|
||||
* No check is performed to ensure the guid exists or the file is present. Simple centralized string builder.
|
||||
*
|
||||
* @param string $guid VideoPress identifier.
|
||||
* @param string $filename name of file associated with the guid (video file name or thumbnail file name).
|
||||
*
|
||||
* @return string Absolute URL of VideoPress file for the given guid.
|
||||
*/
|
||||
function videopress_cdn_file_url( $guid, $filename ) {
|
||||
return "https://videos.files.wordpress.com/{$guid}/{$filename}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the transcoding status for the given video post.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|bool Returns an array of statuses if this is a VideoPress post, otherwise it returns false.
|
||||
*/
|
||||
function videopress_get_transcoding_status( $post_id ) {
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
|
||||
// If this has not been processed by videopress, we can skip the rest.
|
||||
if ( ! $meta || ! isset( $meta['file_statuses'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = (object) $meta['file_statuses'];
|
||||
|
||||
$status = array(
|
||||
'std_mp4' => $info->mp4 ?? null,
|
||||
'std_ogg' => $info->ogg ?? null,
|
||||
'dvd_mp4' => $info->dvd ?? null,
|
||||
'hd_mp4' => $info->hd ?? null,
|
||||
);
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct url to the video.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param string $guid VideoPress GUID.
|
||||
* @return string
|
||||
*/
|
||||
function videopress_build_url( $guid ) {
|
||||
|
||||
// No guid, no videopress url.
|
||||
if ( ! $guid ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'https://videopress.com/v/' . $guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty videopress media item that will be filled out later by an xmlrpc
|
||||
* callback from the VideoPress servers.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param string $title The post_title.
|
||||
* @param string $guid The VideoPress guid.
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
function videopress_create_new_media_item( $title, $guid = null ) {
|
||||
$post = array(
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'video/videopress',
|
||||
'post_title' => $title,
|
||||
'post_content' => '',
|
||||
'guid' => videopress_build_url( $guid ),
|
||||
);
|
||||
|
||||
$media_id = wp_insert_post( $post );
|
||||
|
||||
add_post_meta( $media_id, 'videopress_status', 'initiated' );
|
||||
|
||||
add_post_meta( $media_id, 'videopress_guid', $guid );
|
||||
|
||||
return $media_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge VideoPress file status metadata.
|
||||
*
|
||||
* @param array $current_status The current status of the video.
|
||||
* @param array $new_meta The new meta data to merge with the current status.
|
||||
* @return array
|
||||
*/
|
||||
function videopress_merge_file_status( $current_status, $new_meta ) {
|
||||
$new_statuses = array();
|
||||
|
||||
if ( isset( $new_meta['videopress']['files_status']['hd'] ) ) {
|
||||
$new_statuses['hd'] = $new_meta['videopress']['files_status']['hd'];
|
||||
}
|
||||
|
||||
if ( isset( $new_meta['videopress']['files_status']['dvd'] ) ) {
|
||||
$new_statuses['dvd'] = $new_meta['videopress']['files_status']['dvd'];
|
||||
}
|
||||
|
||||
if ( isset( $new_meta['videopress']['files_status']['std']['mp4'] ) ) {
|
||||
$new_statuses['mp4'] = $new_meta['videopress']['files_status']['std']['mp4'];
|
||||
}
|
||||
|
||||
if ( isset( $new_meta['videopress']['files_status']['std']['ogg'] ) ) {
|
||||
$new_statuses['ogg'] = $new_meta['videopress']['files_status']['std']['ogg'];
|
||||
}
|
||||
|
||||
foreach ( $new_statuses as $format => $status ) {
|
||||
if ( ! isset( $current_status[ $format ] ) ) {
|
||||
$current_status[ $format ] = $status;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $current_status[ $format ] !== 'DONE' ) {
|
||||
$current_status[ $format ] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $current_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a video has completed processing.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
function videopress_is_finished_processing( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post->ID );
|
||||
if ( ! isset( $meta['videopress']['finished'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $meta['videopress']['finished'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the meta information status for the given video post.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
function videopress_update_meta_data( $post_id ) {
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
|
||||
// If this has not been processed by VideoPress, we can skip the rest.
|
||||
if ( ! $meta || ! isset( $meta['videopress'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = (object) $meta['videopress'];
|
||||
|
||||
// Without a guid there is no video to query for.
|
||||
if ( ! isset( $info->guid ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = Client::wpcom_json_api_request_as_blog( 'videos/' . $info->guid );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = json_decode( $result['body'], true );
|
||||
|
||||
// Update the attachment metadata.
|
||||
$meta['videopress'] = $response;
|
||||
|
||||
wp_update_attachment_metadata( $post_id, $meta );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is a VideoPress post that hasn't had a guid set yet.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
function videopress_is_attachment_without_guid( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $post->post_mime_type !== 'video/videopress' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$videopress_guid = get_post_meta( $post_id, 'videopress_guid', true );
|
||||
|
||||
if ( $videopress_guid ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is a VideoPress attachment.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
function is_videopress_attachment( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $post->post_mime_type !== 'video/videopress' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the video update path
|
||||
*
|
||||
* @since 4.4
|
||||
* @param string $guid VideoPress GUID.
|
||||
* @return string
|
||||
*/
|
||||
function videopress_make_video_get_path( $guid ) {
|
||||
return sprintf(
|
||||
'%s/rest/v%s/videos/%s',
|
||||
JETPACK__WPCOM_JSON_API_BASE,
|
||||
Client::WPCOM_JSON_API_VERSION,
|
||||
$guid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the upload api path.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param int $blog_id The id of the blog we're uploading to.
|
||||
* @return string
|
||||
*/
|
||||
function videopress_make_media_upload_path( $blog_id ) {
|
||||
return sprintf(
|
||||
'https://public-api.wordpress.com/rest/v1.1/sites/%s/media/new?locale=%s',
|
||||
$blog_id,
|
||||
get_locale()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resumable upload api path.
|
||||
*
|
||||
* @since 4.4
|
||||
* @param int $blog_id The id of the blog we're uploading to.
|
||||
* @return string
|
||||
*/
|
||||
function videopress_make_resumable_upload_path( $blog_id ) {
|
||||
return sprintf(
|
||||
'https://public-api.wordpress.com/rest/v1.1/video-uploads/%s/',
|
||||
$blog_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a mock of the internal VideoPress method, which is meant to duplicate the functionality
|
||||
* of the WPCOM API, so that the Jetpack REST API returns the same data with no modifications.
|
||||
*
|
||||
* @param int $blog_id Blog ID.
|
||||
* @param int $post_id Post ID.
|
||||
* @return stdClass
|
||||
*/
|
||||
function video_get_info_by_blogpostid( $blog_id, $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
$video_info = new stdClass();
|
||||
$video_info->post_id = 0;
|
||||
$video_info->description = '';
|
||||
$video_info->title = '';
|
||||
$video_info->caption = '';
|
||||
$video_info->blog_id = $blog_id;
|
||||
$video_info->guid = null;
|
||||
$video_info->finish_date_gmt = '0000-00-00 00:00:00';
|
||||
$video_info->rating = null;
|
||||
$video_info->privacy_setting = VIDEOPRESS_PRIVACY::SITE_DEFAULT;
|
||||
|
||||
if ( ! $post ) {
|
||||
return $video_info;
|
||||
}
|
||||
|
||||
$video_info->post_id = $post_id;
|
||||
$video_info->description = $post->post_content;
|
||||
$video_info->title = $post->post_title;
|
||||
$video_info->caption = $post->post_excerpt;
|
||||
|
||||
if ( 'video/videopress' !== $post->post_mime_type ) {
|
||||
return $video_info;
|
||||
}
|
||||
|
||||
// Since this is a VideoPress post, lt's fill out the rest of the object.
|
||||
$video_info->guid = get_post_meta( $post_id, 'videopress_guid', true );
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
|
||||
if ( $meta && isset( $meta['videopress'] ) ) {
|
||||
$videopress_meta = $meta['videopress'];
|
||||
$video_info->rating = $videopress_meta['rating'] ?? null;
|
||||
$video_info->allow_download = $videopress_meta['allow_download'] ?? 0;
|
||||
$video_info->display_embed = $videopress_meta['display_embed'] ?? 0;
|
||||
$video_info->privacy_setting = ! isset( $videopress_meta['privacy_setting'] ) ? VIDEOPRESS_PRIVACY::SITE_DEFAULT : $videopress_meta['privacy_setting'];
|
||||
|
||||
if ( ! empty( $videopress_meta['finished'] ) ) {
|
||||
$video_info->finish_date_gmt = gmdate( 'Y-m-d H:i:s', (int) $videopress_meta['finished'] );
|
||||
}
|
||||
}
|
||||
|
||||
/** Make sure we are keeping some meta keys updated for filtering purposes */
|
||||
if ( get_post_meta( $post_id, 'videopress_rating', true ) !== $video_info->rating ) {
|
||||
update_post_meta( $post_id, 'videopress_rating', $video_info->rating );
|
||||
}
|
||||
if ( get_post_meta( $post_id, 'videopress_privacy_setting', true ) !== $video_info->privacy_setting ) {
|
||||
update_post_meta( $post_id, 'videopress_privacy_setting', $video_info->privacy_setting );
|
||||
}
|
||||
|
||||
return $video_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a VideoPress video format has finished processing.
|
||||
*
|
||||
* This uses the info object, because that is what the WPCOM endpoint
|
||||
* uses, however we don't have a complete info object in the same way
|
||||
* WPCOM does, so we pull the meta information out of the post
|
||||
* options instead.
|
||||
*
|
||||
* Note: This mimics the WPCOM function of the same name and helps the media
|
||||
* API endpoint add all needed VideoPress data.
|
||||
*
|
||||
* @param stdClass $info Info object.
|
||||
* @param string $format Video format.
|
||||
* @return bool
|
||||
*/
|
||||
function video_format_done( $info, $format ) {
|
||||
|
||||
// Avoids notice when a non-videopress item is found.
|
||||
if ( ! is_object( $info ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_id = $info->post_id;
|
||||
|
||||
if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post->ID );
|
||||
|
||||
$video_format = str_replace( array( 'fmt_', 'fmt1_' ), '', $format );
|
||||
|
||||
if ( 'ogg' === $video_format ) {
|
||||
return isset( $meta['videopress']['files']['std']['ogg'] );
|
||||
} else {
|
||||
return isset( $meta['videopress']['files'][ $video_format ]['mp4'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image URL for the given VideoPress GUID
|
||||
*
|
||||
* We look up by GUID, because that is what WPCOM does and this needs to be
|
||||
* parameter compatible with that.
|
||||
*
|
||||
* Note: This mimics the WPCOM function of the same name and helps the media
|
||||
* API endpoint add all needed VideoPress data.
|
||||
*
|
||||
* @param string $guid VideoPress GUID.
|
||||
* @param string $format Video format.
|
||||
* @return string|null The poster image URL, or null if it cannot be resolved.
|
||||
*/
|
||||
function video_image_url_by_guid( $guid, $format ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
$post = videopress_get_post_by_guid( $guid );
|
||||
|
||||
if ( is_wp_error( $post ) || ! $post ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post->ID );
|
||||
|
||||
if ( ! is_array( $meta ) || ! isset( $meta['videopress']['poster'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$poster = apply_filters( 'jetpack_photon_url', $meta['videopress']['poster'] );
|
||||
|
||||
return $poster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using a GUID, find a post.
|
||||
*
|
||||
* @param string $guid The post guid.
|
||||
* @return WP_Post|false The post for that guid, or false if none is found.
|
||||
*/
|
||||
function videopress_get_post_by_guid( $guid ) {
|
||||
$cache_key = 'get_post_by_guid_' . $guid;
|
||||
$cache_group = 'videopress';
|
||||
$cached_post = wp_cache_get( $cache_key, $cache_group );
|
||||
|
||||
if ( is_object( $cached_post ) && 'WP_Post' === get_class( $cached_post ) ) {
|
||||
return $cached_post;
|
||||
}
|
||||
|
||||
$post_id = videopress_get_post_id_by_guid( $guid );
|
||||
|
||||
if ( is_int( $post_id ) ) {
|
||||
$post = get_post( $post_id );
|
||||
wp_cache_set( $cache_key, $post, $cache_group, HOUR_IN_SECONDS );
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using a GUID, find the associated post ID.
|
||||
*
|
||||
* @since 8.4.0
|
||||
* @param string $guid The guid to look for the post ID of.
|
||||
* @return int|false The post ID for that guid, or false if none is found.
|
||||
*/
|
||||
function videopress_get_post_id_by_guid( $guid ) {
|
||||
$cache_key = 'videopress_get_post_id_by_guid_' . $guid;
|
||||
$cached_id = get_transient( $cache_key );
|
||||
|
||||
if ( is_int( $cached_id ) ) {
|
||||
return $cached_id;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'video/videopress',
|
||||
'post_status' => 'inherit',
|
||||
'no_found_rows' => true,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'videopress_guid',
|
||||
'value' => $guid,
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Query( $args );
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
$post_id = $query->next_post();
|
||||
set_transient( $cache_key, $post_id, HOUR_IN_SECONDS );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the given VideoPress post_id, return back the appropriate attachment URL.
|
||||
*
|
||||
* When the MP4 hasn't been processed yet or this is not a VideoPress video, this will return null.
|
||||
*
|
||||
* @param int $post_id Post ID of the attachment.
|
||||
* @return string|null
|
||||
*/
|
||||
function videopress_get_attachment_url( $post_id ) {
|
||||
|
||||
// We only handle VideoPress attachments.
|
||||
if ( get_post_mime_type( $post_id ) !== 'video/videopress' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata( $post_id );
|
||||
|
||||
// As of Jetpack 10.3 transcoded video files are reserved for the VideoPress player.
|
||||
// All other video file requests will receive the originally uploaded file, stored on the wpcom cdn.
|
||||
if ( ! isset( $meta['videopress']['original'] ) ) {
|
||||
// Use the original file as the url if it isn't transcoded yet.
|
||||
if ( isset( $meta['original'] ) ) {
|
||||
$return = $meta['original'];
|
||||
} else {
|
||||
// Otherwise, there isn't much we can do.
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$return = $meta['videopress']['original'];
|
||||
}
|
||||
|
||||
// If the URL is a string, return it. Otherwise, we shouldn't to avoid errors downstream, so null.
|
||||
return ( is_string( $return ) ) ? $return : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts VideoPress flash embeds into oEmbed-able URLs.
|
||||
*
|
||||
* Older VideoPress embed depended on Flash, which no longer work,
|
||||
* so let us convert them to an URL that WordPress can oEmbed.
|
||||
*
|
||||
* Note that this file is always loaded via modules/module-extras.php and is not dependent on module status.
|
||||
*
|
||||
* @param string $content the content.
|
||||
* @return string filtered content
|
||||
*/
|
||||
function jetpack_videopress_flash_embed_filter( $content ) {
|
||||
// This receives data from the `the_content` filter, which unfortunately sometimes has bad content passed along as a param.
|
||||
if ( ! is_string( $content ) ) {
|
||||
return $content;
|
||||
}
|
||||
$regex = '%<embed[^>]*+>(?:\s*</embed>)?%i';
|
||||
$content = preg_replace_callback(
|
||||
$regex,
|
||||
function ( $matches ) {
|
||||
$embed_code = $matches[0];
|
||||
$url_matches = array();
|
||||
|
||||
// get video ID from flash URL.
|
||||
$url_matched = preg_match( '/src="http:\/\/v.wordpress.com\/([^"]+)"/', $embed_code, $url_matches );
|
||||
|
||||
if ( $url_matched ) {
|
||||
$video_id = $url_matches[1];
|
||||
return "https://videopress.com/v/$video_id";
|
||||
}
|
||||
},
|
||||
$content
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided rating string is a valid VideoPress video rating value.
|
||||
*
|
||||
* @param mixed $rating The video rating to validate.
|
||||
* @return bool
|
||||
*/
|
||||
function videopress_is_valid_video_rating( $rating ) {
|
||||
return in_array( $rating, array( 'G', 'PG-13', 'R-17' ), true );
|
||||
}
|
||||
|
||||
add_filter( 'the_content', 'jetpack_videopress_flash_embed_filter', 7 ); // Needs to be priority 7 to allow Core to oEmbed.
|
||||
|
||||
if ( ! function_exists( 'wp_startswith' ) ) :
|
||||
/**
|
||||
* Check whether a string starts with a specific substring.
|
||||
*
|
||||
* @param string $haystack String we are filtering.
|
||||
* @param string $needle The substring we are looking for.
|
||||
* @return bool
|
||||
*/
|
||||
function wp_startswith( $haystack, $needle ) {
|
||||
if ( ! $haystack || ! $needle || ! is_scalar( $haystack ) || ! is_scalar( $needle ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$haystack = (string) $haystack;
|
||||
$needle = (string) $needle;
|
||||
|
||||
return str_starts_with( $haystack, $needle );
|
||||
}
|
||||
endif;
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Divi 5 integration bootstrap for VideoPress.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5;
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Package_Version;
|
||||
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
|
||||
use ET\Builder\VisualBuilder\Assets\PackageBuildManager;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the VideoPress module into the Divi 5 framework.
|
||||
*
|
||||
* Both hooks below are only fired by Divi 5, so this integration stays inert on
|
||||
* Divi 4 (where the legacy module continues to handle rendering).
|
||||
*/
|
||||
class Divi_5 {
|
||||
|
||||
/**
|
||||
* Registers the Divi 5 hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'divi_module_library_modules_dependency_tree', array( __CLASS__, 'register_module' ) );
|
||||
add_action( 'divi_visual_builder_assets_before_enqueue_scripts', array( __CLASS__, 'enqueue_visual_builder_assets' ) );
|
||||
|
||||
/*
|
||||
* Divi builds its module library during theme bootstrap, which fires the
|
||||
* dependency-tree action above (once) before this integration loads on
|
||||
* `init` — so the callback misses it on requests like the Divi 5 Migrator's
|
||||
* admin-ajax calls. Register directly here too; VideoPress_Module::load()
|
||||
* is idempotent, so the two paths never double-register.
|
||||
*/
|
||||
if ( class_exists( ModuleRegistration::class, false ) ) {
|
||||
( new VideoPress_Module() )->load();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the VideoPress module to the Divi 5 module dependency tree.
|
||||
*
|
||||
* @param object $dependency_tree The Divi 5 module dependency tree.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_module( $dependency_tree ) {
|
||||
$dependency_tree->add_dependency( new VideoPress_Module() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the Visual Builder bundle that powers the module's editing UI.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enqueue_visual_builder_assets() {
|
||||
$asset_file = dirname( __DIR__, 2 ) . '/build/divi-5/index.asset.php';
|
||||
$asset = file_exists( $asset_file ) ? require $asset_file : array();
|
||||
|
||||
PackageBuildManager::register_package_build(
|
||||
array(
|
||||
'name' => 'jetpack-videopress-divi5-visual-builder',
|
||||
// The build content hash, so each build busts the browser cache.
|
||||
'version' => $asset['version'] ?? Package_Version::PACKAGE_VERSION,
|
||||
'script' => array(
|
||||
'src' => plugins_url( '../../build/divi-5/index.js', __FILE__ ),
|
||||
|
||||
/*
|
||||
* The handles the build emits (react, react-jsx-runtime and the
|
||||
* Divi-vendored @wordpress/* instances), plus the Divi builder
|
||||
* handles that provide the window.divi.* globals.
|
||||
*/
|
||||
'deps' => array_merge(
|
||||
array( 'divi-module-library', 'divi-rest' ),
|
||||
$asset['dependencies'] ?? array()
|
||||
),
|
||||
'enqueue_top_window' => false,
|
||||
'enqueue_app_window' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5;
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Divi5\Traits\Module_Classnames_Trait;
|
||||
use Automattic\Jetpack\VideoPress\Divi5\Traits\Module_Script_Data_Trait;
|
||||
use Automattic\Jetpack\VideoPress\Divi5\Traits\Module_Styles_Trait;
|
||||
use Automattic\Jetpack\VideoPress\Divi5\Traits\Render_Callback_Trait;
|
||||
use ET\Builder\Framework\DependencyManagement\Interfaces\DependencyInterface;
|
||||
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and renders the VideoPress module within the Divi 5 framework.
|
||||
*
|
||||
* The `ET\Builder\*` symbols this class relies on only exist while Divi 5 is
|
||||
* active, which is why the class is loaded lazily from hooks that Divi 5 fires.
|
||||
*/
|
||||
class VideoPress_Module implements DependencyInterface {
|
||||
|
||||
use Render_Callback_Trait;
|
||||
use Module_Classnames_Trait;
|
||||
use Module_Styles_Trait;
|
||||
use Module_Script_Data_Trait;
|
||||
|
||||
/**
|
||||
* Matches a VideoPress URL or GUID and captures the GUID. Kept in sync with
|
||||
* the regex used by the Visual Builder renderer.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VIDEOPRESS_REGEX = '/^(?:(?:http(?:s)?:\/\/)?(?:www\.)?video(?:\.word)?press\.com\/(?:v|embed)\/)?([a-zA-Z\d]+)(?:.*)?/i';
|
||||
|
||||
/**
|
||||
* The module's block name. Must match the `name` field in `module.json`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MODULE_NAME = 'jetpack/videopress';
|
||||
|
||||
/**
|
||||
* Registers the module with the Divi 5 module library.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load() {
|
||||
/*
|
||||
* Divi walks its dependency tree more than once per request, and
|
||||
* Divi_5::init() also calls this directly, so guard on the registry to
|
||||
* register exactly once. Registering before `init` matches how Divi
|
||||
* registers its own modules during theme bootstrap.
|
||||
*/
|
||||
if ( \WP_Block_Type_Registry::get_instance()->is_registered( self::MODULE_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read from build/ (shipped) rather than src/client/ (production-excluded).
|
||||
ModuleRegistration::register_module(
|
||||
dirname( __DIR__, 2 ) . '/build/divi-5/modules/videopress',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_callback' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom CSS fields for the Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5\Traits;
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Divi5\VideoPress_Module;
|
||||
use WP_Block_Type;
|
||||
use WP_Block_Type_Registry;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the module's custom CSS fields, as declared in `module.json`.
|
||||
*/
|
||||
trait Custom_Css_Trait {
|
||||
|
||||
/**
|
||||
* Returns the registered custom CSS fields for the module.
|
||||
*
|
||||
* @return array The custom CSS field definitions.
|
||||
*/
|
||||
public static function custom_css() {
|
||||
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( VideoPress_Module::MODULE_NAME );
|
||||
|
||||
if ( ! $block_type instanceof WP_Block_Type ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property defined by the Divi 5 framework.
|
||||
return $block_type->customCssFields ?? array();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Class name output for the Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5\Traits;
|
||||
|
||||
use ET\Builder\Packages\Module\Options\Element\ElementClassnames;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the module's class names, mirroring the Visual Builder classnames function.
|
||||
*/
|
||||
trait Module_Classnames_Trait {
|
||||
|
||||
/**
|
||||
* Adds the module's decoration class names.
|
||||
*
|
||||
* @param array $args The classnames callback arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function module_classnames( $args ) {
|
||||
$classnames_instance = $args['classnamesInstance'];
|
||||
$attrs = $args['attrs'];
|
||||
|
||||
$classnames_instance->add(
|
||||
ElementClassnames::classnames(
|
||||
array(
|
||||
'attrs' => $attrs['module']['decoration'] ?? array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Front-end script data for the Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5\Traits;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the module's front-end script data, mirroring the Visual Builder
|
||||
* script data component.
|
||||
*/
|
||||
trait Module_Script_Data_Trait {
|
||||
|
||||
/**
|
||||
* Registers the module's script data.
|
||||
*
|
||||
* @param array $args The script data callback arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function module_script_data( $args ) {
|
||||
$elements = $args['elements'];
|
||||
|
||||
$elements->script_data(
|
||||
array(
|
||||
'attrName' => 'module',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* Style output for the Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5\Traits;
|
||||
|
||||
use ET\Builder\FrontEnd\Module\Style;
|
||||
use ET\Builder\Packages\Module\Options\Css\CssStyle;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the module's styles, mirroring the Visual Builder style component.
|
||||
*/
|
||||
trait Module_Styles_Trait {
|
||||
|
||||
use Custom_Css_Trait;
|
||||
|
||||
/**
|
||||
* Adds the module's styles to the style registry.
|
||||
*
|
||||
* @param array $args The style callback arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function module_styles( $args ) {
|
||||
$attrs = $args['attrs'] ?? array();
|
||||
$elements = $args['elements'];
|
||||
$settings = $args['settings'] ?? array();
|
||||
|
||||
Style::add(
|
||||
array(
|
||||
'id' => $args['id'] ?? '',
|
||||
'name' => $args['name'] ?? '',
|
||||
'orderIndex' => $args['orderIndex'] ?? 0,
|
||||
'storeInstance' => $args['storeInstance'] ?? null,
|
||||
'styles' => array(
|
||||
$elements->style(
|
||||
array(
|
||||
'attrName' => 'module',
|
||||
'styleProps' => array(
|
||||
'disabledOn' => array(
|
||||
'disabledModuleVisibility' => $settings['disabledModuleVisibility'] ?? null,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
CssStyle::style(
|
||||
array(
|
||||
'selector' => $args['orderClass'],
|
||||
'attr' => $attrs['css'] ?? array(),
|
||||
'cssFields' => self::custom_css(),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Render callback for the Divi 5 VideoPress module.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\Jetpack\VideoPress\Divi5\Traits;
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Divi5\VideoPress_Module;
|
||||
use Automattic\Jetpack\VideoPress\Jwt_Token_Bridge;
|
||||
use Automattic\Jetpack\VideoPress\Package_Version;
|
||||
use ET\Builder\FrontEnd\BlockParser\BlockParserStore;
|
||||
use ET\Builder\Packages\Module\Module;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the front-end markup for the VideoPress module.
|
||||
*/
|
||||
trait Render_Callback_Trait {
|
||||
|
||||
/**
|
||||
* Renders the module on the front end.
|
||||
*
|
||||
* @param array $attrs The module attributes.
|
||||
* @param string $content The module content.
|
||||
* @param object $block The parsed block object.
|
||||
* @param object $elements The module element helpers.
|
||||
*
|
||||
* @return string The rendered HTML, or an empty string when no valid GUID is set.
|
||||
*/
|
||||
public static function render_callback( $attrs, $content, $block, $elements ) {
|
||||
$guid_value = $attrs['guid']['innerContent']['desktop']['value'] ?? '';
|
||||
|
||||
$matches = array();
|
||||
if ( ! preg_match( VideoPress_Module::VIDEOPRESS_REGEX, (string) $guid_value, $matches ) || ! isset( $matches[1] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
|
||||
|
||||
$video_player = self::render_video_player( $matches[1], $attrs );
|
||||
$parent = BlockParserStore::get_parent( $block->parsed_block['id'], $block->parsed_block['storeInstance'] );
|
||||
|
||||
return Module::render(
|
||||
array(
|
||||
'orderIndex' => $block->parsed_block['orderIndex'],
|
||||
'storeInstance' => $block->parsed_block['storeInstance'],
|
||||
'attrs' => $attrs,
|
||||
'elements' => $elements,
|
||||
'id' => $block->parsed_block['id'],
|
||||
'moduleClassName' => '',
|
||||
'name' => $block->block_type->name,
|
||||
'classnamesFunction' => array( VideoPress_Module::class, 'module_classnames' ),
|
||||
'moduleCategory' => $block->block_type->category,
|
||||
'stylesComponent' => array( VideoPress_Module::class, 'module_styles' ),
|
||||
'scriptDataComponent' => array( VideoPress_Module::class, 'module_script_data' ),
|
||||
'parentAttrs' => $parent->attrs ?? array(),
|
||||
'parentId' => $parent->id ?? '',
|
||||
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property defined by the Divi 5 framework.
|
||||
'parentName' => $parent->blockName ?? '',
|
||||
'children' => $elements->style_components(
|
||||
array(
|
||||
'attrName' => 'module',
|
||||
)
|
||||
) . $video_player,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the VideoPress iframe wrapper markup for a GUID.
|
||||
*
|
||||
* @param string $guid The VideoPress GUID.
|
||||
* @param array $attrs The module attributes, used to derive player options.
|
||||
*
|
||||
* @return string The iframe wrapper markup.
|
||||
*/
|
||||
private static function render_video_player( $guid, $attrs ) {
|
||||
/*
|
||||
* Enqueue the shared VideoPress iframe API bootstrap rather than printing a
|
||||
* <script> per render. This reuses the `videopress-iframe` handle the
|
||||
* VideoPress block registers, so the script loads once per page no matter
|
||||
* how many videos (or blocks) are present.
|
||||
*/
|
||||
wp_enqueue_script(
|
||||
'videopress-iframe',
|
||||
'https://videopress.com/videopress-iframe.js',
|
||||
array(),
|
||||
Package_Version::PACKAGE_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
$iframe_title = sprintf(
|
||||
/* translators: %s: Video GUID. */
|
||||
esc_html__( 'Video player for %s', 'jetpack-videopress-pkg' ),
|
||||
esc_html( $guid )
|
||||
);
|
||||
|
||||
$iframe_src = self::build_embed_url( $guid, $attrs );
|
||||
|
||||
return '<div class="vidi-videopress-wrapper" style="position:relative;width:100%;height:0;padding-bottom:56.25%;">' .
|
||||
'<iframe title="' . esc_attr( $iframe_title ) . '" src="' . esc_url( $iframe_src ) . '" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" frameborder="0" allowfullscreen></iframe>' .
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the VideoPress embed URL, applying only the player options that differ
|
||||
* from the VideoPress player defaults (for a clean URL, mirroring the block).
|
||||
* Kept in sync with `getEmbedUrl()` in the Visual Builder's `utils.js`.
|
||||
*
|
||||
* @param string $guid The VideoPress GUID.
|
||||
* @param array $attrs The module attributes.
|
||||
*
|
||||
* @return string The embed URL.
|
||||
*/
|
||||
private static function build_embed_url( $guid, $attrs ) {
|
||||
$options = self::get_player_options( $attrs );
|
||||
|
||||
$params = array();
|
||||
if ( $options['autoplay'] ) {
|
||||
$params['autoPlay'] = '1';
|
||||
}
|
||||
if ( $options['loop'] ) {
|
||||
$params['loop'] = '1';
|
||||
}
|
||||
if ( $options['muted'] ) {
|
||||
$params['muted'] = '1';
|
||||
$params['persistVolume'] = '0';
|
||||
}
|
||||
if ( ! $options['controls'] ) {
|
||||
$params['controls'] = '0';
|
||||
}
|
||||
if ( $options['playsinline'] ) {
|
||||
$params['playsinline'] = '1';
|
||||
}
|
||||
// Always identify the embedder so VideoPress can attribute Divi traffic.
|
||||
$params['embedder'] = 'divi-builder';
|
||||
|
||||
return add_query_arg( $params, 'https://videopress.com/embed/' . rawurlencode( $guid ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the module's player options from its attributes, falling back to the
|
||||
* VideoPress player defaults when a toggle is unset.
|
||||
*
|
||||
* @param array $attrs The module attributes.
|
||||
*
|
||||
* @return array<string, bool> The resolved player options.
|
||||
*/
|
||||
private static function get_player_options( $attrs ) {
|
||||
return array(
|
||||
'autoplay' => self::is_toggle_on( $attrs, 'autoplay', false ),
|
||||
'loop' => self::is_toggle_on( $attrs, 'loop', false ),
|
||||
'muted' => self::is_toggle_on( $attrs, 'muted', false ),
|
||||
'controls' => self::is_toggle_on( $attrs, 'controls', true ),
|
||||
'playsinline' => self::is_toggle_on( $attrs, 'playsinline', false ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Divi 5 toggle attribute, which stores `'on'`/`'off'` strings.
|
||||
*
|
||||
* @param array $attrs The module attributes.
|
||||
* @param string $name The attribute name.
|
||||
* @param bool $default_value The value to use when the toggle is unset.
|
||||
*
|
||||
* @return bool Whether the toggle is on.
|
||||
*/
|
||||
private static function is_toggle_on( $attrs, $name, $default_value ) {
|
||||
$value = $attrs[ $name ]['innerContent']['desktop']['value'] ?? ( $default_value ? 'on' : 'off' );
|
||||
|
||||
return 'on' === $value;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Divi integration for Videopress.
|
||||
*
|
||||
* @package automattic/jetpack-videopress
|
||||
**/
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi extension.
|
||||
**/
|
||||
class VideoPress_Divi_Extension extends DiviExtension {
|
||||
|
||||
/**
|
||||
* The extension's js file namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const JETPACK_VIDEOPRESS_DIVI_PKG_NAMESPACE = 'jetpack-videopress-divi-pkg';
|
||||
|
||||
/**
|
||||
* The gettext domain for the extension's translations.
|
||||
*
|
||||
* @since 0.14.0
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $gettext_domain = 'jetpack-videopress-pkg';
|
||||
|
||||
/**
|
||||
* The extension's WP Plugin name.
|
||||
*
|
||||
* @since 0.14.0
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'videopress-divi';
|
||||
|
||||
/**
|
||||
* The extension's version
|
||||
*
|
||||
* @since 0.14.0
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $version = '1.0.0';
|
||||
|
||||
/**
|
||||
* The VideoPress_Divi_Module.
|
||||
*
|
||||
* @since 0.14.0
|
||||
*
|
||||
* @var VideoPress_Divi_Module
|
||||
*/
|
||||
private $videopress_divi_module;
|
||||
|
||||
/**
|
||||
* Plugin directory.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $plugin_dir;
|
||||
|
||||
/**
|
||||
* Plugin directory URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $plugin_dir_url;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $name The name.
|
||||
* @param array $args The args.
|
||||
*/
|
||||
public function __construct( $name = 'videopress-divi', $args = array() ) {
|
||||
$this->plugin_dir = plugin_dir_path( __FILE__ );
|
||||
$this->plugin_dir_url = plugin_dir_url( $this->plugin_dir );
|
||||
|
||||
parent::__construct( $name, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when the ET builder module is loaded.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public function hook_et_builder_modules_loaded() {
|
||||
$this->hook_et_builder_ready();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads custom modules when the builder is ready.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public function hook_et_builder_ready() {
|
||||
require_once plugin_dir_path( __FILE__ ) . 'class-videopress-divi-module.php';
|
||||
$this->videopress_divi_module = new VideoPress_Divi_Module();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs initialization tasks.
|
||||
*
|
||||
* @Override
|
||||
*/
|
||||
protected function _initialize() { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
|
||||
DiviExtensions::add( $this );
|
||||
|
||||
$this->_set_debug_mode();
|
||||
$this->_set_bundle_dependencies();
|
||||
|
||||
// Register callbacks.
|
||||
register_activation_hook( trailingslashit( $this->plugin_dir ) . $this->name . '.php', array( $this, 'wp_hook_activate' ) );
|
||||
|
||||
add_action( 'et_builder_ready', array( $this, 'hook_et_builder_ready' ), 9 );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'wp_hook_enqueue_scripts' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'register_js_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the extension's js assets.
|
||||
*/
|
||||
public function register_js_assets() {
|
||||
Assets::register_script(
|
||||
self::JETPACK_VIDEOPRESS_DIVI_PKG_NAMESPACE,
|
||||
'../../build/divi-editor/index.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'css_path' => '../../build/divi-editor/index.css',
|
||||
'textdomain' => 'jetpack-videopress-pkg',
|
||||
'dependencies' => array( 'jquery' ),
|
||||
)
|
||||
);
|
||||
|
||||
Assets::enqueue_script( self::JETPACK_VIDEOPRESS_DIVI_PKG_NAMESPACE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend stuff.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public function wp_hook_enqueue_scripts() {
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/**
|
||||
* VideoPress Divi Editor module.
|
||||
*
|
||||
* @package VideoPress
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\VideoPress\Jwt_Token_Bridge;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* VideoPress Divi module
|
||||
**/
|
||||
class VideoPress_Divi_Module extends ET_Builder_Module {
|
||||
|
||||
/**
|
||||
* Module slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $slug = 'divi_videopress';
|
||||
|
||||
/**
|
||||
* For matching VideoPress urls or guids.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VIDEOPRESS_REGEX = '/^(?:(?:http(?:s)?:\/\/)?(?:www\.)?video(?:\.word)?press\.com\/(?:v|embed)\/)?([a-zA-Z\d]+)(?:.*)?/i';
|
||||
|
||||
/**
|
||||
* Vd support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $vb_support = 'on';
|
||||
|
||||
/**
|
||||
* Credits.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $module_credits = array(
|
||||
'module_uri' => 'https://automattic.com',
|
||||
'author' => 'Automattic Inc',
|
||||
'author_uri' => 'https://automattic.com',
|
||||
);
|
||||
|
||||
/**
|
||||
* Name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Icon.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $icon;
|
||||
|
||||
/**
|
||||
* Properties.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $props;
|
||||
|
||||
/**
|
||||
* Initialize the thing.
|
||||
*/
|
||||
public function init() {
|
||||
$this->name = esc_html__( 'VideoPress', 'jetpack-videopress-pkg' );
|
||||
$this->icon = 'q';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields of the block.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_fields() {
|
||||
return array(
|
||||
'guid' => array(
|
||||
'label' => esc_html__( 'URL or Video ID', 'jetpack-videopress-pkg' ),
|
||||
'type' => 'text',
|
||||
'option_category' => 'basic_option',
|
||||
'description' => esc_html__( 'Paste a VideoPress URL or Video ID', 'jetpack-videopress-pkg' ),
|
||||
'toggle_slug' => 'main_content',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
* Note: while phpcs complains about unused vars in the method signature, they SHOULD be there. We override an existing method.
|
||||
*
|
||||
* @param array $attrs The attributes.
|
||||
* @param string|null $content The content.
|
||||
* @param string|null $render_slug The render slug.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render( $attrs, $content = null, $render_slug = null ) { // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$matches = array();
|
||||
|
||||
if ( ! preg_match( self::VIDEOPRESS_REGEX, $this->props['guid'], $matches ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! isset( $matches[1] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
|
||||
|
||||
$guid = $matches[1];
|
||||
$iframe_title = sprintf(
|
||||
/* translators: %s: Video title. */
|
||||
esc_html__( 'Video player for %s', 'jetpack-videopress-pkg' ),
|
||||
esc_html( $guid )
|
||||
);
|
||||
|
||||
$iframe_src = sprintf(
|
||||
'https://videopress.com/embed/%s?autoPlay=0&permalink=0&loop=0&embedder=divi-builder',
|
||||
esc_attr( $guid )
|
||||
);
|
||||
$format_string = '<div class="vidi-videopress-wrapper"><iframe title="' .
|
||||
esc_attr( $iframe_title ) .
|
||||
'" src="' .
|
||||
$iframe_src .
|
||||
'" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>' .
|
||||
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
|
||||
'<script src="https://en.wordpress.com/wp-content/plugins/video/assets/js/next/videopress-iframe.js?m=1658739239"></script></div>';
|
||||
|
||||
return $format_string;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user