<?php
/**
* Cloudinary API wrapper.
*
* @package Cloudinary
*/
namespace Cloudinary\Connect;
use Cloudinary\Relate\Relationship;
use Cloudinary\Sync;
use Cloudinary\Utils;
use Cloudinary\Plugin;
use Cloudinary\Media;
use function Cloudinary\get_plugin_instance;
/**
* Class API.
*
* Push media to Cloudinary on upload.
*/
class Api {
/**
* The cloudinary credentials array.
*
* @var array
*/
public $credentials;
/**
* Cloudinary Asset URL.
*
* @var string
*/
public $asset_url = 'res.cloudinary.com';
/**
* Cloudinary API Version.
*
* @var string
*/
public $api_version = 'v1_1';
/**
* Plugin Version
*
* @var string
*/
public $plugin_version;
/**
* Holds the media instance.
*
* @var Media
*/
protected $media;
/**
* List of cloudinary transformations.
*
* @var array
*/
public static $transformation_index = array(
'image' => array(
'a' => 'angle',
'ar' => 'aspect_ratio',
'b' => 'background',
'bo' => 'border',
'c' => 'crop',
'co' => 'color',
'dpr' => 'dpr',
'du' => 'duration',
'e' => 'effect',
'eo' => 'end_offset',
'fl' => 'flags',
'h' => 'height',
'l' => 'overlay',
'o' => 'opacity',
'q' => 'quality',
'r' => 'radius',
'so' => 'start_offset',
't' => 'named_transformation',
'u' => 'underlay',
'vc' => 'video_codec',
'w' => 'width',
'x' => 'x',
'y' => 'y',
'z' => 'zoom',
'ac' => 'audio_codec',
'af' => 'audio_frequency',
'br' => 'bit_rate',
'cs' => 'color_space',
'd' => 'default_image',
'dl' => 'delay',
'dn' => 'density',
'f' => 'fetch_format',
'g' => 'gravity',
'p' => 'prefix',
'pg' => 'page',
'sp' => 'streaming_profile',
'vs' => 'video_sampling',
'if' => 'if',
),
'video' => array(
'w' => 'width',
'h' => 'height',
'c' => 'crop',
'ar' => 'aspect_ratio',
'g' => 'gravity',
'b' => 'background',
'e' => 'effect',
'l' => 'overlay',
'so' => 'start_offset',
'eo' => 'end_offset',
'du' => 'duration',
'a' => 'angle',
'vs' => 'video_sampling',
'dl' => 'delay',
'vc' => 'video_codec',
'fps' => 'fps',
'dpr' => 'dpr',
'br' => 'bit_rate',
'ki' => 'keyframe_interval',
'sp' => 'streaming_profile',
'ac' => 'audio_codec',
'af' => 'audio_frequency',
'fl' => 'flags',
'f' => 'fetch_format',
'q' => 'quality',
'if' => 'if',
),
);
/**
* Cloudinary qualified upload prefixes to date.
*
* @var string[]
*/
public static $qualified_upload_prefixes = array(
'api.cloudinary.com',
'api-eu.cloudinary.com',
'api-ap.cloudinary.com',
);
/**
* Current pending url to overide the fields to post.
*
* @var string|null
*/
private $pending_url = array();
/**
* API constructor.
*
* @param \Cloudinary\Connect $connect The connect object.
* @param string $version The plugin version.
*/
public function __construct( $connect, $version ) {
$this->credentials = $connect->get_credentials();
$this->plugin_version = $version;
// Use CNAME.
if ( ! empty( $this->credentials['cname'] ) ) {
$this->asset_url = $this->credentials['cname'];
}
add_action( 'cloudinary_ready', array( $this, 'setup' ) );
}
/**
* Setup the API
*
* @param Plugin $plugin The plugin instance.
*/
public function setup( Plugin $plugin ) {
$this->media = $plugin->get_component( 'media' );
}
/**
* Return an endpoint for a specific resource type.
*
* @param string $resource The resource type for the endpoint.
* @param string $function The function of the endpoint.
* @param bool $endpoint Flag to get an endpoint or an asset url.
*
* @return string
*/
public function url( $resource, $function = null, $endpoint = false ) {
$parts = array();
if ( $endpoint ) {
$parts[] = $this->get_upload_prefix();
$parts[] = $this->api_version;
} else {
$parts[] = $this->asset_url;
}
if ( empty( $this->credentials['cname'] ) || $endpoint || 'false' === $this->credentials['private_cdn'] ) {
$parts[] = $this->credentials['cloud_name'];
}
/**
* Bypass Cloudinary's SEO URLs.
*
* @hook cloudinary_bypass_seo_url
* @since 3.1.5
*
* @param $bypass_seo_url {bool} Whether to bypass SEO URLs.
*
* @return {bool}
*/
$bypass_seo_url = apply_filters( 'cloudinary_bypass_seo_url', false );
if ( false === $endpoint && 'image' === $resource && 'upload' === $function && ! $bypass_seo_url ) {
$parts[] = 'images';
} else {
$parts[] = $resource;
$parts[] = $function;
}
$parts = array_filter( $parts );
$url = implode( '/', $parts );
return $url;
}
/**
* Generate a transformation string.
*
* @param array $options The transformation options to generate from.
* @param string $type The asset Type.
* @param string $context The context.
*
* @return string
*/
public static function generate_transformation_string( array $options, $type = 'image', $context = '' ) {
if ( ! isset( self::$transformation_index[ $type ] ) ) {
return '';
}
$transformation_index = self::$transformation_index[ $type ];
$transformations = array_map(
function ( $item ) use ( $transformation_index ) {
$transform = array();
if ( is_string( $item ) ) {
return $item;
}
foreach ( $item as $type => $value ) { // phpcs:ignore
$key = array_search( $type, $transformation_index, true );
if ( false !== $key ) {
$transform[] = $key . '_' . $value;
} elseif ( '$' === $type[0] ) {
$transform[] = $type . '_' . $value;
}
}
return implode( ',', $transform );
},
$options
);
// Prepare the eager transformations for the upload.
if ( 'upload' === $context ) {
foreach ( $transformations as &$transformation ) {
if ( 0 <= strpos( $transformation, 'f_auto' ) ) {
$parts = explode( ',', $transformation );
unset( $parts[ array_search( 'f_auto', $parts, true ) ] );
$remaining_transformations = implode( ',', $parts );
$formats = array();
if ( 'image' === $type ) {
$formats = array(
'f_avif',
'f_webp',
'f_webp,fl_awebp',
'f_wdp',
'f_jp2',
);
} elseif ( 'video' === $type ) {
$formats = array(
'f_webm,vc_vp9',
'f_mp4,vc_h265',
'f_mp4,vc_h264',
);
}
/**
* Filter the upload eager formats.
*
* @hook cloudinary_upload_eager_formats
* @since 3.1.6
*
* @param $formats {array} The default formats.
* @param $type {string} The asset type.
*
* @return {array}
*/
$formats = apply_filters( 'cloudinary_upload_eager_formats', $formats, $type );
array_walk(
$formats,
static function ( &$i ) use ( $remaining_transformations ) {
if ( $remaining_transformations ) {
$i .= ",{$remaining_transformations}";
}
}
);
$transformation = implode( '|', $formats );
}
}
}
// Clear out empty parts.
$transformations = array_filter( $transformations );
return implode( '/', $transformations );
}
/**
* Generate a Cloudinary URL.
*
* @param string|null $public_id The Public ID to get a url for.
* @param array $args Additional args.
* @param array $size The WP Size array.
* @param int|null $attachment_id The attachment ID.
*
* @return string
*/
public function cloudinary_url( $public_id = null, $args = array(), $size = array(), $attachment_id = null ) {
if ( null === $public_id ) {
return 'https://' . $this->url( null, null );
}
$defaults = array(
'resource_type' => 'image',
'delivery' => 'upload',
'version' => 'v1',
);
$args = wp_parse_args( array_filter( $args ), $defaults );
// check for version.
if ( ! empty( $args['version'] ) && is_numeric( $args['version'] ) ) {
$args['version'] = 'v' . $args['version'];
}
// Determine if we're dealing with a fetched.
// ...or uploaded image and update the URL accordingly.
$asset_endpoint = filter_var( $public_id, FILTER_VALIDATE_URL ) ? 'fetch' : $args['delivery'];
$url_parts = array(
'https:/',
$this->url( $args['resource_type'], $asset_endpoint ),
);
$base = Utils::pathinfo( $public_id );
// Add size.
if ( ! empty( $size ) && is_array( $size ) ) {
if ( ! empty( $size['transformation'] ) ) {
$url_parts[] = $size['transformation'];
}
// add size to ID if scaled.
if ( ! empty( $size['file'] ) ) {
$public_id = str_replace( $base['basename'], $size['file'], $public_id );
}
}
if ( ! empty( $args['transformation'] ) ) {
$url_parts[] = self::generate_transformation_string( $args['transformation'], $args['resource_type'] );
}
if ( $attachment_id ) {
$public_id = $this->get_public_id( $attachment_id, $args, $public_id );
}
$url_parts[] = $args['version'];
$url_parts[] = $public_id;
// Clear out empty parts.
$url_parts = array_filter( $url_parts );
return implode( '/', $url_parts );
}
/**
* Get the details of an asset by public ID.
*
* @param string $public_id The public_id to check.
* @param string $type The asset type.
*
* @return array|\WP_Error
*/
public function get_asset_details( $public_id, $type ) {
$url = $this->url( 'resources', $type . '/upload/' . $public_id, true );
return $this->call( $url, array( 'body' => array() ), 'get' );
}
/**
* Upload a large asset in chunks.
*
* @param int $attachment_id The attachment ID.
* @param array $args Array of upload options.
*
* @return array|\WP_Error
*/
public function upload_large( $attachment_id, $args ) {
// Ensure we have the right file.
if ( empty( $args['file'] ) ) {
$args['file'] = get_attached_file( $attachment_id );
}
$tempfile = false;
if ( false !== strpos( $args['file'], 'vip://' ) ) {
$args['file'] = $this->create_local_copy( $args['file'] );
if ( is_wp_error( $args['file'] ) ) {
return $args['file'];
}
$tempfile = true;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
global $wp_filesystem;
if ( ! in_array( $wp_filesystem->method, array( 'vip', 'direct' ), true ) ) {
// We'll need to have direct file access to be able to read a chunked version to upload.
// Perhaps we could use the URL upload method in this case?
return new \WP_Error( 'upload_error', __( 'No direct access to file system.', 'cloudinary' ) );
}
// Since WP_Filesystem doesn't have a fread, we need to do it manually. However we'll still use it for writing.
$src = fopen( $args['file'], 'r' ); // phpcs:ignore
$temp_file_name = wp_tempnam( uniqid( time() ) . '.' . Utils::pathinfo( $args['file'], PATHINFO_EXTENSION ) );
$upload_id = substr( sha1( uniqid( $this->credentials['api_secret'] . wp_rand() ) ), 0, 16 );
$chunk_size = 20000000;
$index = 0;
$file_size = filesize( $args['file'] );
while ( ! feof( $src ) ) {
$current_loc = $index * $chunk_size;
if ( $current_loc >= $file_size ) {
break;
}
$data = fread( $src, $chunk_size ); // phpcs:ignore
file_put_contents( $temp_file_name, $data ); //phpcs:ignore
clearstatcache( true, $temp_file_name );
$temp_file_size = filesize( $temp_file_name );
$range = 'bytes ' . $current_loc . '-' . ( $current_loc + $temp_file_size - 1 ) . '/' . $file_size;
$headers = array(
'Content-Range' => $range,
'X-Unique-Upload-Id' => $upload_id,
);
$args['file'] = $temp_file_name;
$result = $this->upload( $attachment_id, $args, $headers );
if ( is_wp_error( $result ) ) {
break;
}
++$index;
}
fclose( $src ); //phpcs:ignore
unlink( $temp_file_name ); //phpcs:ignore
if ( true === $tempfile ) {
unlink( $args['file'] ); //phpcs:ignore
}
return $result;
}
/**
* Copy an asset from one Cloudinary location to another.
*
* @param int $attachment_id Attachment ID to upload.
* @param array $args Array of upload options.
*
* @return array|\WP_Error
*/
public function copy( $attachment_id, $args ) {
$resource = ! empty( $args['resource_type'] ) ? $args['resource_type'] : 'image';
$url = $this->url( $resource, 'upload', true );
$args = $this->clean_args( $args );
$args['file'] = $this->media->raw_cloudinary_url( $attachment_id );
$call_args = array(
'headers' => array(),
'body' => $args,
);
return $this->call( $url, $call_args, 'post' );
}
/**
* Upload an asset.
*
* @param int $attachment_id Attachment ID to upload.
* @param array $args Array of upload options.
* @param array $headers Additional headers to use in upload.
* @param bool $try_remote Flag to try_remote upload.
*
* @return array|\WP_Error
*/
public function upload( $attachment_id, $args, $headers = array(), $try_remote = true ) {
$resource = ! empty( $args['resource_type'] ) ? $args['resource_type'] : 'image';
$url = $this->url( $resource, 'upload', true );
$args = $this->clean_args( $args );
$disable_https_fetch = get_transient( '_cld_disable_http_upload' );
/**
* Whether to use the original image URL.
*
* @hook cloudinary_use_original_image
* @since 3.1.8
* @default true
*
* @param $use_original {bool} The default value.
* @param $attachment_id {int} The attachment ID.
*
* @return {bool}
*/
$use_original = apply_filters( 'cloudinary_use_original_image', true, $attachment_id );
if ( $use_original && function_exists( 'wp_get_original_image_url' ) && wp_attachment_is_image( $attachment_id ) ) {
$file_url = wp_get_original_image_url( $attachment_id );
} else {
$file_url = wp_get_attachment_url( $attachment_id );
}
if ( empty( $file_url ) ) {
$disable_https_fetch = true;
}
if ( ! $this->media ) {
$this->media = get_plugin_instance()->get_component( 'media' );
}
if ( $this->media && ! $this->media->is_uploadable_media( $attachment_id ) ) {
$disable_https_fetch = false; // Remote can upload via url.
// translators: variable is thread name and queue size.
$action_message = sprintf( __( 'Uploading remote url: %1$s.', 'cloudinary' ), $file_url );
do_action( '_cloudinary_queue_action', $action_message );
}
$tempfile = false;
if ( $this->media && $this->media->is_cloudinary_url( $file_url ) ) {
// If this is a Cloudinary URL, then we can use it to fetch from that location.
$disable_https_fetch = false;
}
// Check if we can try http file upload.
if ( ( empty( $headers ) && empty( $disable_https_fetch ) && true === $try_remote ) || ! $this->media->is_uploadable_media( $attachment_id ) ) {
$args['file'] = $file_url;
} elseif ( ! $this->media->is_local_media( $attachment_id ) ) {
$args['file'] = $file_url;
} else {
// We should have the file in args at this point, but if the transient was set, it will be defaulting here.
if ( empty( $args['file'] ) ) {
if ( wp_attachment_is_image( $attachment_id ) ) {
$get_path_func = $use_original && function_exists( 'wp_get_original_image_path' ) ? 'wp_get_original_image_path' : 'get_attached_file';
$args['file'] = call_user_func( $get_path_func, $attachment_id );
} else {
$args['file'] = get_attached_file( $attachment_id );
}
}
// Headers indicate chunked upload.
if ( empty( $headers ) && file_exists( $args['file'] ) ) {
$size = filesize( $args['file'] );
if ( 'video' === $resource || $size > 100000000 ) {
return $this->upload_large( $attachment_id, $args );
}
}
if ( false !== strpos( $args['file'], 'vip://' ) ) {
$args['file'] = $this->create_local_copy( $args['file'] );
if ( is_wp_error( $args['file'] ) ) {
return $args['file'];
}
$tempfile = true;
}
// Attach File.
if ( function_exists( 'curl_file_create' ) ) {
$file = $args['file'];
$args['file'] = curl_file_create( $file ); // phpcs:ignore
$args['file']->setPostFilename( $file );
} else {
$args['file'] = '@' . $args['file'];
}
}
$call_args = array(
'headers' => $headers,
'body' => $args,
);
/**
* Filter Cloudinary upload args.
*
* @hook cloudinary_upload_args
* @since 3.0.1
*
* @param $call_args {array} The default args.
* @param $attachment_id {int} The attachment ID.
*/
$call_args = apply_filters( 'cloudinary_upload_args', $call_args, $attachment_id );
$result = $this->call( $url, $call_args, 'post' );
// Hook in flag to allow for non accessible URLS.
if ( is_wp_error( $result ) ) {
$error = $result->get_error_message();
$code = $result->get_error_code();
/**
* If there's an error and the file is a URL in the error message,
* it's likely due to CURL or the location does not support URL file attachments.
* In this case, we'll flag and disable it and try again with a local file.
*/
if ( 404 !== $code && empty( $disable_https_fetch ) && false !== strpos( urldecode( $error ), $args['file'] ) ) {
// URLS are not remotely available, try again as a file.
set_transient( '_cld_disable_http_upload', true, DAY_IN_SECONDS );
// Remove URL file.
unset( $args['file'] );
return $this->upload( $attachment_id, $args );
}
}
if ( true === $tempfile && is_string( $args['file'] ) ) {
unlink( $args['file'] ); //phpcs:ignore
}
/**
* Action after uploaded asset.
*
* @hook cloudinary_uploaded_asset
* @since 3.0.1
*
* @param $attachment_id {int} The attachment ID.
* @param $result {array|WP_Error} The upload result.
*/
do_action( 'cloudinary_uploaded_asset', $attachment_id, $result );
return $result;
}
/**
* Upload an cache item.
*
* @param array $args The upload parameters.
*
* @return array $the url to the cached item.
*/
public function upload_cache( $args ) {
$call_args = array(
'headers' => array(),
'body' => $args,
);
$url = $this->url( 'auto', 'upload', true );
return $this->call( $url, $call_args, 'post' );
}
/**
* Create a local copy of the file if stored remotely in VIP.
*
* @param string $file File name to copy.
*
* @return string|\WP_Error
*/
public function create_local_copy( $file ) {
$file_copy = wp_tempnam( wp_basename( $file ) );
$content = file_get_contents( $file ); //phpcs:ignore
if ( file_put_contents( $file_copy, $content ) ) { //phpcs:ignore
$file = $file_copy;
} else {
return new \WP_Error( 'upload_fail', __( 'Could not get VIP file content', 'cloudinary' ) );
}
return $file;
}
/**
* Expicit update of an asset.
*
* @param array $args Array of options to update.
*
* @return array|\WP_Error
*/
public function explicit( $args ) {
$url = $this->url( 'image', 'explicit', true );
$args = $this->clean_args( $args );
return $this->call( $url, array( 'body' => $args ), 'post' );
}
/**
* Destroy an asset.
*
* @param string $type The resource type to destroy.
* @param array $options Array of options.
*
* @return array|\WP_Error
*/
public function destroy( $type, $options ) {
$url = $this->url( $type, 'destroy', true );
return $this->call( $url, array( 'body' => $options ), 'post' );
}
/**
* Rename an asset.
*
* @param string $type The resource type to rename.
* @param array $options Array of options.
*
* @return array|\WP_Error
*/
public function rename( $type, $options ) {
$url = $this->url( $type, 'rename', true );
return $this->call( $url, array( 'body' => $options ), 'post' );
}
/**
* Context update of an asset.
*
* @param array $args Array of options to update.
*
* @return array|\WP_Error
*/
public function context( $args ) {
$url = $this->url( $args['resource_type'], 'context', true );
$options = array(
'public_ids' => $args['public_id'],
'context' => $args['context'],
'command' => 'add',
);
$options = $this->clean_args( $options );
return $this->call( $url, array( 'body' => $options ), 'post' );
}
/**
* Clean the args before sending to endpoint.
*
* @param array $args Array of args to clean.
*
* @return array
*/
public function clean_args( $args ) {
return array_map(
function ( $value ) {
if ( is_array( $value ) ) {
$value = wp_json_encode( $value );
}
if ( is_bool( $value ) ) {
$value = true === $value ? '1' : '0';
}
return $value;
},
$args
);
}
/**
* General Call based on tag.
*
* @param string $name Name of method to call.
* @param array $args Array of parameters to pass to call.
*
* @return array|\WP_Error
*/
public function __call( $name, $args ) {
$function = null;
if ( ! empty( $args[0] ) ) {
$function = $args[0];
}
$url = $this->url( $name, $function, true );
$method = 'get';
$send_args = array( 'body' => $args );
if ( ! empty( $args[1] ) ) {
$method = $args[1];
}
if ( ! empty( $args[2] ) ) {
$send_args['body'] = $args[2];
}
return $this->call( $url, $send_args, $method );
}
/**
* Sign a request
*
* @param array $args Array of parameters to sign.
*
* @return array|\WP_Error
*/
public function sign( $args ) {
// Sort parameters.
ksort( $args );
$args = array_map(
function ( $value, $key ) {
$remove = array( 'file', 'resource_type', 'api_key' );
if ( in_array( $key, $remove, true ) || '' === $value ) {
return null;
}
if ( is_array( $value ) ) {
$value = implode( ',', array_values( $value ) );
}
return $key . '=' . $value;
},
$args,
array_keys( $args )
);
$to_sign = array_filter( $args );
$string = implode( '&', $to_sign );
return sha1( $string . $this->credentials['api_secret'] );
}
/**
* Set the POSTFIELDS to the correct array type, not the string based.
*
* @param \Requests_Transport_cURL $handle The transport handle to set.
* @param array $request The request array.
* @param string $url The url to send to.
*/
public function set_data( $handle, $request, $url ) {
// Ensure that this request is in fact ours.
if ( $this->pending_url === $url ) {
curl_setopt( $handle, CURLOPT_POSTFIELDS, $request['body'] ); // phpcs:ignore
$this->pending_url = null;
}
}
/**
* Get Cloudinary upload prefix.
*
* @return string
*/
public function get_upload_prefix() {
static $upload_prefix;
if ( is_null( $upload_prefix ) ) {
// Defaults to first hardcoded qualified domain.
$upload_prefix = reset( self::$qualified_upload_prefixes );
// Maybe use constant.
if ( self::is_qualified_upload_prefix( CLOUDINARY_ENDPOINTS_API ) ) {
$upload_prefix = self::get_hostname( CLOUDINARY_ENDPOINTS_API );
}
// Maybe use query argument.
if (
! empty( $this->credentials['upload_prefix'] )
&& self::is_qualified_upload_prefix( $this->credentials['upload_prefix'] )
) {
$upload_prefix = self::get_hostname( $this->credentials['upload_prefix'] );
}
}
return $upload_prefix;
}
/**
* Get the Cloudinary public_id.
*
* @param int $attachment_id The attachment ID.
* @param array $args The args.
* @param null|string $original_public_id The original public ID.
*
* @return string
*/
public function get_public_id( $attachment_id, $args = array(), $original_public_id = null ) {
$relationship = Relationship::get_relationship( $attachment_id );
$public_id = null;
if ( $relationship instanceof Relationship ) {
$public_id = $relationship->public_id;
}
// On cases like the initial sync, we might not have a relationship, so we need to trust to requested public_id.
if ( empty( $public_id ) ) {
$public_id = $original_public_id;
}
/**
* Bypass Cloudinary's SEO URLs.
*
* @hook cloudinary_bypass_seo_url
* @since 3.1.5
*
* @param $bypass_seo_url {bool} Whether to bypass SEO URLs.
*
* @return {bool}
*/
$bypass_seo_url = apply_filters( 'cloudinary_bypass_seo_url', false );
if (
$public_id
&& ! $bypass_seo_url
&& (
// Get the SEO `public_id` for images with `upload` delivery.
(
! empty( $args['resource_type'] ) && 'image' === $args['resource_type']
&& ! empty( $args['delivery'] ) && 'upload' === $args['delivery']
)
// Get the SEO `public_id` for PDFs as they are regarded as images.
|| (
! empty( $args['resource_type'] ) && 'image' === $args['resource_type']
&& 'application' === $relationship->asset_type
)
)
) {
$parts = explode( '/', $public_id );
$filename = end( $parts );
/**
* Filter the SEO public ID.
*
* @hook cloudinary_seo_public_id
* @since 3.1.5
*
* @param $sufix {string} The public_id suffix.
* @param $relationship {Relationship} The relationship.
* @param $attachment_id {int} The attachment ID.
*
* @return {string}
*/
$suffix = apply_filters( 'cloudinary_seo_public_id', "{$filename}.{$relationship->format}", $relationship, $attachment_id );
$public_id .= "/{$suffix}";
}
if ( 'video' === $relationship->asset_type ) {
$public_id .= ".{$relationship->format}";
}
return $public_id;
}
/**
* Check if a string is a qualified `upload_prefix`.
*
* @param string $upload_prefix Upload prefix to check.
*
* @return bool
*/
protected static function is_qualified_upload_prefix( $upload_prefix ) {
$hostname = self::get_hostname( $upload_prefix );
return in_array( $hostname, self::$qualified_upload_prefixes, true );
}
/**
* Get the hostname from a string.
*
* @param string $upload_prefix Upload prefix to check.
*
* @return string
*/
protected static function get_hostname( $upload_prefix ) {
$hostname = $upload_prefix;
if ( filter_var( $upload_prefix, FILTER_VALIDATE_URL ) ) {
$hostname = wp_parse_url( $upload_prefix, PHP_URL_HOST );
}
return $hostname;
}
/**
* Calls the API request.
*
* @param string $url The url to call.
* @param array $args The optional arguments to send.
* @param string $method The call HTTP method.
*
* @return array|\WP_Error
*/
private function call( $url, $args = array(), $method = 'get' ) {
$args['method'] = strtoupper( $method );
$args['user-agent'] = 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . ' (' . $this->plugin_version . ')';
$args['headers']['referer'] = Utils::site_url();
if ( 'GET' === $args['method'] ) {
$url = 'https://' . $this->credentials['api_key'] . ':' . $this->credentials['api_secret'] . '@' . $url;
} else {
$url = 'https://' . $url;
$args['body']['api_key'] = $this->credentials['api_key'];
$args['body']['timestamp'] = time();
// Sign request.
$args['body']['signature'] = $this->sign( $args['body'] );
ksort( $args['body'] );
// Fix the data to not be a flattend fields string.
add_action( 'http_api_curl', array( $this, 'set_data' ), 10, 3 );
// Add url to list to allow it to be fixed.
$this->pending_url = $url;
}
// Set a long-ish timeout since uploads can be 20mb+.
$args['timeout'] = 90; // phpcs:ignore
// Adjust timeout for additional eagers if image_freeform or video_freeform is set.
if ( ! empty( $args['body']['resource_type'] ) ) {
$freeform = $this->media->get_settings()->get_value( $args['body']['resource_type'] . '_freeform' );
if ( ! empty( $freeform ) ) {
$timeout_multiplier = explode( '/', $freeform );
$args['timeout'] += 60 * count( $timeout_multiplier ); // phpcs:ignore
}
}
$request = wp_remote_request( $url, $args );
if ( is_wp_error( $request ) ) {
return $request;
}
$body = wp_remote_retrieve_body( $request );
$result = json_decode( $body, ARRAY_A );
if ( empty( $result ) && ! empty( $body ) ) {
return $body; // not json.
}
if ( ! empty( $result['error'] ) && ! empty( $result['error']['message'] ) ) {
return new \WP_Error( $request['response']['code'], $result['error']['message'] );
}
return $result;
}
}