View file File name : class-tus-client.php Content :<?php /** * Tus_Client * * @package VideoPressUploader **/ // phpcs:disable Generic.Commenting.DocComment.MissingShort // phpcs:disable Squiz.Commenting.VariableComment.Missing // 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 ); } }