Files
WPS3Media/classes/items/item.php
Malin 3248cbb029 feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.

Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
  - Provider key: s3compatible
  - Reads user-configured endpoint URL from settings
  - Uses path-style URL access (required by most S3-compatible services)
  - Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
    AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
  - Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
2026-03-03 12:30:18 +01:00

2198 lines
61 KiB
PHP

<?php
namespace DeliciousBrains\WP_Offload_Media\Items;
use Amazon_S3_And_CloudFront;
use AS3CF_Error;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider;
use Exception;
use WP_Error;
abstract class Item {
const ITEMS_TABLE = 'as3cf_items';
const ORIGINATORS = array(
'standard' => 0,
'metadata-tool' => 1,
);
const CAN_USE_OBJECT_VERSIONING = true;
protected static $source_type_name = 'Item';
protected static $source_type = '';
protected static $source_table = '';
protected static $source_fk = '';
protected static $summary_type_name = '';
protected static $summary_type = '';
protected static $can_use_yearmonth = true;
protected static $items_cache_by_id = array();
protected static $items_cache_by_source_id = array();
protected static $items_cache_by_path = array();
protected static $items_cache_by_source_path = array();
protected static $item_counts = array();
protected static $item_count_skips = array();
/**
* @var array Keys with array of fields that can be used for cache lookups.
*/
protected static $cache_keys = array(
'id' => array( 'id' ),
'source_id' => array( 'source_id' ),
'path' => array( 'path', 'original_path' ),
'source_path' => array( 'source_path', 'original_source_path' ),
);
private static $checked_table_exists = array();
private static $enable_cache = true;
private $id;
private $provider;
private $region;
private $bucket;
private $path;
private $original_path;
private $is_private;
private $source_id;
private $source_path;
private $original_source_path;
private $extra_info;
private $originator;
private $is_verified;
/**
* Item constructor.
*
* @param string $provider Storage provider key name, e.g. "aws".
* @param string $region Region for item's bucket.
* @param string $bucket Bucket for item.
* @param string $path Key path for item (full sized if type has thumbnails etc).
* @param bool $is_private Is the object private in the bucket.
* @param int $source_id ID that source has.
* @param string $source_path Path that source uses, could be relative or absolute depending on source.
* @param string $original_filename An optional filename with no path that was previously used for the item.
* @param array $extra_info An optional array of extra data specific to the source type.
* @param int $id Optional Item record ID.
* @param int $originator Optional originator of record from ORIGINATORS const.
* @param bool $is_verified Optional flag as to whether Item's objects are known to exist.
* @param bool $use_object_versioning Optional flag as to whether path prefix should use Object Versioning if type allows it.
*/
public function __construct(
$provider,
$region,
$bucket,
$path,
$is_private,
$source_id,
$source_path,
$original_filename = null,
$extra_info = array(),
$id = null,
$originator = 0,
$is_verified = true,
$use_object_versioning = self::CAN_USE_OBJECT_VERSIONING
) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$this->source_id = $source_id;
$this->source_path = $source_path;
if ( empty( $original_filename ) ) {
$this->original_source_path = $source_path;
} else {
$this->original_source_path = str_replace( wp_basename( $source_path ), $original_filename, $source_path );
}
// Set offload data from previous duplicate if exact match by source path exists.
if ( empty( $path ) ) {
$prev_items = static::get_by_source_path( array( $this->source_path, $this->original_source_path ), $this->source_id, true, true );
if ( ! is_wp_error( $prev_items ) && ! empty( $prev_items[0] ) && is_a( $prev_items[0], get_class( $this ) ) ) {
/** @var Item $prev_item */
$prev_item = $prev_items[0];
$provider = $prev_item->provider();
$region = $prev_item->region();
$bucket = $prev_item->bucket();
$path = $prev_item->path();
$is_private = $prev_item->is_private();
$extra_info = $prev_item->extra_info();
}
}
// Not a duplicate, create a new path to offload to.
if ( empty( $path ) ) {
$prefix = $this->get_new_item_prefix( $use_object_versioning );
$path = $prefix . wp_basename( $source_path );
}
if ( ! is_array( $extra_info ) ) {
$extra_info = array();
}
if ( ! isset( $extra_info['private_prefix'] ) || is_null( $extra_info['private_prefix'] ) ) {
$extra_info['private_prefix'] = '';
if ( $as3cf->private_prefix_enabled() ) {
$extra_info['private_prefix'] = AS3CF_Utils::trailingslash_prefix( $as3cf->get_setting( 'signed-urls-object-prefix', '' ) );
}
}
if ( empty( $provider ) ) {
$provider = $as3cf->get_storage_provider()->get_provider_key_name();
}
if ( empty( $region ) ) {
$region = $as3cf->get_setting( 'region' );
if ( is_wp_error( $region ) ) {
$region = '';
}
}
if ( empty( $bucket ) ) {
$bucket = $as3cf->get_setting( 'bucket' );
}
$this->provider = $provider;
$this->region = $region;
$this->bucket = $bucket;
$this->path = $path;
$this->extra_info = $extra_info;
$this->originator = $originator;
$this->is_verified = $is_verified;
if ( empty( $original_filename ) ) {
$this->original_path = $path;
} else {
$this->original_path = str_replace( wp_basename( $path ), $original_filename, $path );
}
if ( ! empty( $id ) ) {
$this->id = $id;
}
$this->set_is_private( (bool) $is_private );
static::add_to_items_cache( $this );
}
/**
* Returns the standard object key for an items primary object
*
* @return string
*/
public static function primary_object_key() {
return '__as3cf_primary';
}
/**
* Enable the built-in Item cache.
*/
public static function enable_cache() {
self::$enable_cache = true;
}
/**
* Disable the built-in Item cache.
*/
public static function disable_cache() {
self::$enable_cache = false;
}
/**
* Returns the string used to group all keys in the object cache by.
*
* @return string
*/
protected static function get_object_cache_group() {
static $group;
if ( empty( $group ) ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
/**
* Filters the object cache group name.
*
* @param string $group Defaults to 'as3cf'
*/
$group = trim( '' . apply_filters( 'as3cf_object_cache_group', $as3cf->get_plugin_prefix() ) );
}
return $group;
}
/**
* Get base string for all of current blog's object cache keys.
*
* @return string
*/
protected static function get_object_cache_base_key() {
$blog_id = get_current_blog_id();
return static::items_table() . '-' . $blog_id . '-' . static::$source_type;
}
/**
* Get full object cache key.
*
* @param string $base_key
* @param string $key
* @param string $field
*
* @return string
*/
protected static function get_object_cache_full_key( $base_key, $key, $field ) {
return sanitize_text_field( $base_key . '-' . $key . '-' . $field );
}
/**
* Add the given item to the object cache.
*
* @param Item $item
*/
protected static function add_to_object_cache( $item ) {
if ( empty( $item ) || empty( static::$cache_keys ) ) {
return;
}
$base_key = static::get_object_cache_base_key();
$group = static::get_object_cache_group();
$keys = array();
foreach ( static::$cache_keys as $key => $fields ) {
foreach ( $fields as $field ) {
$full_key = static::get_object_cache_full_key( $base_key, $key, $item->{$field}() );
if ( in_array( $full_key, $keys ) ) {
continue;
}
wp_cache_set( $full_key, $item, $group );
$keys[] = $full_key;
}
}
}
/**
* Delete the given item from the object cache.
*
* @param Item $item
*/
protected static function remove_from_object_cache( $item ) {
if ( empty( $item ) || empty( static::$cache_keys ) ) {
return;
}
$base_key = static::get_object_cache_base_key();
$group = static::get_object_cache_group();
$keys = array();
foreach ( static::$cache_keys as $key => $fields ) {
foreach ( $fields as $field ) {
$full_key = static::get_object_cache_full_key( $base_key, $key, $item->{$field}() );
if ( in_array( $full_key, $keys ) ) {
continue;
}
wp_cache_delete( $full_key, $group );
$keys[] = $full_key;
}
}
}
/**
* Try and get Item from object cache by known key and value.
*
* Note: Actual lookup is scoped by blog and item's source_type, so example key may be 'source_id'.
*
* @param string $key The base of the key that makes up the lookup, e.g. field for given value.
* @param mixed $value Will be coerced to string for lookup.
*
* @return bool|Item
*/
protected static function get_from_object_cache( $key, $value ) {
if ( ! array_key_exists( $key, static::$cache_keys ) ) {
return false;
}
$base_key = static::get_object_cache_base_key();
$full_key = static::get_object_cache_full_key( $base_key, $key, $value );
$group = static::get_object_cache_group();
$force = false;
$found = false;
$result = wp_cache_get( $full_key, $group, $force, $found );
if ( $found ) {
return $result;
}
return false;
}
/**
* (Re)initialize the static cache used for speeding up queries.
*/
public static function init_cache() {
self::$checked_table_exists = array();
static::$items_cache_by_id = array();
static::$items_cache_by_source_id = array();
static::$items_cache_by_path = array();
static::$items_cache_by_source_path = array();
static::$item_counts = array();
static::$item_count_skips = array();
}
/**
* Add an item to the static cache to allow fast retrieval via get_from_items_cache_by_* functions.
*
* @param Item $item
*/
protected static function add_to_items_cache( $item ) {
$blog_id = get_current_blog_id();
if ( ! empty( $item->id() ) ) {
static::$items_cache_by_id[ $blog_id ][ $item->id() ] = $item;
}
if ( ! empty( $item->source_id() ) ) {
static::$items_cache_by_source_id[ $blog_id ][ static::$source_type ][ $item->source_id() ] = $item;
}
if ( ! empty( $item->path() ) ) {
static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $item->original_path() ] = $item;
static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $item->path() ] = $item;
}
if ( ! empty( $item->source_path() ) ) {
static::$items_cache_by_source_path[ $blog_id ][ static::$source_type ][ $item->original_source_path() ] = $item;
static::$items_cache_by_source_path[ $blog_id ][ static::$source_type ][ $item->source_path() ] = $item;
}
}
/**
* Remove an item from the static cache that allows fast retrieval via get_from_items_cache_by_* functions.
*
* @param Item $item
*/
protected static function remove_from_items_cache( $item ) {
$blog_id = get_current_blog_id();
if ( ! empty( $item->id() ) ) {
unset( static::$items_cache_by_id[ $blog_id ][ $item->id() ] );
}
if ( ! empty( $item->source_id() ) ) {
unset( static::$items_cache_by_source_id[ $blog_id ][ static::$source_type ][ $item->source_id() ] );
}
if ( ! empty( $item->path() ) ) {
unset( static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $item->original_path() ] );
unset( static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $item->path() ] );
}
if ( ! empty( $item->source_path() ) ) {
unset( static::$items_cache_by_source_path[ $blog_id ][ static::$source_type ][ $item->original_source_path() ] );
unset( static::$items_cache_by_source_path[ $blog_id ][ static::$source_type ][ $item->source_path() ] );
}
}
/**
* Try and get Item from cache by known id.
*
* @param int $id
*
* @return bool|Item
*/
private static function get_from_items_cache_by_id( $id ) {
if ( false === self::$enable_cache ) {
return false;
}
$blog_id = get_current_blog_id();
if ( ! empty( static::$items_cache_by_id[ $blog_id ][ $id ] ) ) {
return static::$items_cache_by_id[ $blog_id ][ $id ];
}
$item = static::get_from_object_cache( 'id', $id );
if ( $item ) {
static::add_to_items_cache( $item );
return $item;
}
return false;
}
/**
* Try and get Item from cache by known source_id.
*
* @param int $source_id
*
* @return bool|Item
*/
private static function get_from_items_cache_by_source_id( $source_id ) {
if ( false === self::$enable_cache ) {
return false;
}
$blog_id = get_current_blog_id();
if ( ! empty( static::$items_cache_by_source_id[ $blog_id ][ static::$source_type ][ $source_id ] ) ) {
return static::$items_cache_by_source_id[ $blog_id ][ static::$source_type ][ $source_id ];
}
$item = static::get_from_object_cache( 'source_id', $source_id );
if ( $item ) {
static::add_to_items_cache( $item );
return $item;
}
return false;
}
/**
* Try and get Item from cache by known bucket and path.
*
* @param string $bucket
* @param string $path
*
* @return bool|Item
*/
private static function get_from_items_cache_by_bucket_and_path( $bucket, $path ) {
if ( false === self::$enable_cache ) {
return false;
}
$blog_id = get_current_blog_id();
if ( ! empty( static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $path ] ) ) {
/** @var Item $item */
$item = static::$items_cache_by_path[ $blog_id ][ static::$source_type ][ $path ];
if ( $item->bucket() === $bucket ) {
return $item;
}
}
return false;
}
/**
* The full items table name for current blog.
*
* @return string
*/
public static function items_table(): string {
global $wpdb;
/* @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$table_name = $wpdb->get_blog_prefix() . static::ITEMS_TABLE;
if ( empty( self::$checked_table_exists[ $table_name ] ) ) {
self::$checked_table_exists[ $table_name ] = true;
$schema_version = get_option( $as3cf->get_plugin_prefix() . '_schema_version', '0.0.0' );
if ( version_compare( $schema_version, $as3cf->get_plugin_version(), '<' ) ) {
self::install_table( $table_name );
update_option( $as3cf->get_plugin_prefix() . '_schema_version', $as3cf->get_plugin_version() );
}
}
return $table_name;
}
/**
* Create the table needed by this class with given name (for current site).
*
* @param string $table_name
*/
private static function install_table( $table_name ) {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$wpdb->hide_errors();
$charset_collate = $wpdb->get_charset_collate();
$sql = "
CREATE TABLE {$table_name} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
provider VARCHAR(18) NOT NULL,
region VARCHAR(255) NOT NULL,
bucket VARCHAR(255) NOT NULL,
path VARCHAR(1024) NOT NULL,
original_path VARCHAR(1024) NOT NULL,
is_private BOOLEAN NOT NULL DEFAULT 0,
source_type VARCHAR(18) NOT NULL,
source_id BIGINT(20) UNSIGNED NOT NULL,
source_path VARCHAR(1024) NOT NULL,
original_source_path VARCHAR(1024) NOT NULL,
extra_info LONGTEXT,
originator TINYINT UNSIGNED NOT NULL DEFAULT 0,
is_verified BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY uidx_path (path(190), id),
UNIQUE KEY uidx_original_path (original_path(190), id),
UNIQUE KEY uidx_source_path (source_path(190), id),
UNIQUE KEY uidx_original_source_path (original_source_path(190), id),
UNIQUE KEY uidx_source (source_type, source_id),
UNIQUE KEY uidx_provider_bucket (provider, bucket(190), id),
UNIQUE KEY uidx_is_verified_originator (is_verified, originator, id)
) $charset_collate;
";
dbDelta( $sql );
}
/**
* Get item's data as an array, optionally with id if available.
*
* @param bool $include_id Default false.
*
* @return array
*/
public function key_values( $include_id = false ) {
$key_values = array(
'provider' => $this->provider,
'region' => $this->region,
'bucket' => $this->bucket,
'path' => $this->path,
'original_path' => $this->original_path,
'is_private' => $this->is_private,
'source_type' => static::$source_type,
'source_id' => $this->source_id,
'source_path' => $this->source_path,
'original_source_path' => $this->original_source_path,
'extra_info' => serialize( $this->extra_info ),
'originator' => $this->originator,
'is_verified' => $this->is_verified,
);
if ( $include_id && ! empty( $this->id ) ) {
$key_values['id'] = $this->id;
}
ksort( $key_values );
return $key_values;
}
/**
* Get item's column formats as an associative array, optionally with id if available.
*
* @param bool $include_id Default false.
*
* @return array
*/
private function key_formats( $include_id = false ) {
$key_values = array(
'provider' => '%s',
'region' => '%s',
'bucket' => '%s',
'path' => '%s',
'original_path' => '%s',
'is_private' => '%d',
'source_type' => '%s',
'source_id' => '%d',
'source_path' => '%s',
'original_source_path' => '%s',
'extra_info' => '%s',
'originator' => '%d',
'is_verified' => '%d',
);
if ( $include_id && ! empty( $this->id ) ) {
$key_values['id'] = '%d';
}
ksort( $key_values );
return $key_values;
}
/**
* All the item's column formats in an indexed array, optionally with id if available.
*
* @param bool $include_id Default false.
*
* @return array
*/
private function formats( $include_id = false ) {
return array_values( $this->key_formats( $include_id ) );
}
/**
* Save the item's current data.
*
* @param bool $update_duplicates If updating, also update records for duplicated source, defaults to true.
*
* @return int|WP_Error
*/
public function save( $update_duplicates = true ) {
global $wpdb;
$update = false;
if ( empty( $this->id ) ) {
$result = $wpdb->insert( static::items_table(), $this->key_values(), $this->formats() );
if ( $result ) {
$this->id = $wpdb->insert_id;
// Now that the item has an ID it should be (re)cached.
static::add_to_items_cache( $this );
}
} else {
$update = true;
// Make sure object cache does not have stale items.
$old_item = static::get_from_object_cache( 'id', $this->id() );
static::remove_from_object_cache( $old_item );
unset( $old_item );
$result = $wpdb->update( static::items_table(), $this->key_values(), array( 'id' => $this->id ), $this->formats(), array( '%d' ) );
}
if ( false !== $result ) {
// Now that the item has an ID it should be (re)cached.
static::add_to_object_cache( $this );
} else {
static::remove_from_items_cache( $this );
return new WP_Error( 'item_save', 'Error saving item:- ' . $wpdb->last_error );
}
// If one or more duplicate exists that still has the same source paths, keep them in step.
if ( $update && $update_duplicates ) {
$duplicates = static::get_by_source_path( array( $this->source_path, $this->original_source_path ), $this->source_id );
if ( ! empty( $duplicates ) && ! is_wp_error( $duplicates ) ) {
/** @var Item $duplicate */
foreach ( $duplicates as $duplicate ) {
if (
! is_wp_error( $duplicate ) &&
$duplicate->source_type() === $this->source_type() &&
$duplicate->source_path() === $this->source_path() &&
$duplicate->original_source_path() === $this->original_source_path()
) {
$duplicate->provider = $this->provider;
$duplicate->region = $this->region;
$duplicate->bucket = $this->bucket;
$duplicate->path = $this->path;
$duplicate->original_path = $this->original_path;
$duplicate->is_private = $this->is_private;
$duplicate->extra_info = $this->extra_info;
$duplicate->originator = $this->originator;
$duplicate->is_verified = $this->is_verified;
$duplicate->save( false );
}
}
}
}
return $this->id;
}
/**
* Delete the current item.
*
* @return bool|WP_Error
*/
public function delete() {
global $wpdb;
static::remove_from_items_cache( $this );
static::remove_from_object_cache( $this );
if ( empty( $this->id ) ) {
return new WP_Error( 'item_delete', 'Error trying to delete item with no id.' );
} else {
$result = $wpdb->delete( static::items_table(), array( 'id' => $this->id ), array( '%d' ) );
}
if ( ! $result ) {
return new WP_Error( 'item_delete', 'Error deleting item:- ' . $wpdb->last_error );
}
return true;
}
/**
* Creates an item based on object from database.
*
* @param object $object
* @param bool $add_to_object_cache Should this object be added to the object cache too?
*
* @return Item
*/
protected static function create( $object, $add_to_object_cache = false ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$extra_info = array();
if ( ! empty( $object->extra_info ) ) {
$extra_info = AS3CF_Utils::maybe_unserialize( $object->extra_info );
static::maybe_update_extra_info( $extra_info, $object->source_id, $object->is_private );
}
if ( ! empty( static::$source_type ) && static::$source_type !== $object->source_type ) {
AS3CF_Error::log( sprintf( 'Doing it wrong! Trying to create a %s class instance with data representing a %s', __CLASS__, $object->source_type ) );
}
if ( empty( static::$source_type ) ) {
/** @var Item $class */
$class = $as3cf->get_source_type_class( $object->source_type );
} else {
/** @var Item $class */
$class = $as3cf->get_source_type_class( static::$source_type );
}
$item = new $class(
$object->provider,
$object->region,
$object->bucket,
$object->path,
$object->is_private,
$object->source_id,
$object->source_path,
wp_basename( $object->original_source_path ),
$extra_info,
$object->id,
$object->originator,
$object->is_verified
);
if ( $add_to_object_cache ) {
$class::add_to_object_cache( $item );
}
return $item;
}
/**
* Get an item by its id.
*
* @param integer $id
*
* @return bool|Item
*/
public static function get_by_id( $id ) {
global $wpdb;
if ( empty( $id ) ) {
return false;
}
$item = static::get_from_items_cache_by_id( $id );
if ( ! empty( $item ) ) {
return $item;
}
$sql = $wpdb->prepare( "SELECT * FROM " . static::items_table() . " WHERE source_type = %s AND id = %d", static::$source_type, $id );
$object = $wpdb->get_row( $sql );
if ( empty( $object ) ) {
return false;
}
return static::create( $object, true );
}
/**
* Get an item by its source id.
*
* While source id isn't strictly unique, it is by source type, which is always used in queries based on called class.
*
* @param int $source_id
*
* @return bool|Item
*/
public static function get_by_source_id( $source_id ) {
global $wpdb;
if ( ! is_numeric( $source_id ) ) {
return false;
}
$source_id = (int) $source_id;
if ( $source_id < 0 ) {
return false;
}
$item = static::get_from_items_cache_by_source_id( $source_id );
if ( ! empty( $item ) && ! empty( $item->id() ) ) {
return $item;
}
$sql = $wpdb->prepare( "SELECT * FROM " . static::items_table() . " WHERE source_id = %d AND source_type = %s", $source_id, static::$source_type );
$object = $wpdb->get_row( $sql );
if ( empty( $object ) ) {
return false;
}
return static::create( $object, true );
}
/**
* Getter for item's source type.
*
* @return string
*/
public static function source_type() {
return static::$source_type;
}
/**
* Getter for item's source type name.
*
* @return string
*/
public static function source_type_name() {
return static::$source_type_name;
}
/**
* Getter for item's summary type.
*
* @return string
*/
public static function summary_type(): string {
return static::$summary_type;
}
/**
* Getter for item's summary type name.
*
* @return string
*/
public static function summary_type_name(): string {
return static::$summary_type_name;
}
/**
* Is the item able to be included in a summary?
*
* @return bool
*/
public static function summary_enabled(): bool {
return ! empty( static::summary_type() ) && ! empty( static::summary_type_name() );
}
/**
* Getter for item's id value.
*
* @return integer
*/
public function id() {
return $this->id;
}
/**
* Getter for item's provider value.
*
* @return string
*/
public function provider() {
return $this->provider;
}
/**
* Getter for item's region value.
*
* @return string
*/
public function region() {
return $this->region;
}
/**
* Setter for item's region value.
*
* @param string $region
*/
public function set_region( $region ) {
$this->region = $region;
}
/**
* Getter for item's bucket value.
*
* @return string
*/
public function bucket() {
return $this->bucket;
}
/**
* Setter for item's bucket value.
*
* @param string $bucket
*/
public function set_bucket( $bucket ) {
$this->bucket = $bucket;
}
/**
* Getter for item's path value.
*
* The path is always the public representation,
* see provider_key() and provider_keys() for realised versions.
*
* @param string $object_key
*
* @return string
*/
public function path( $object_key = null ) {
$path = $this->path;
if ( ! empty( $object_key ) ) {
$objects = $this->objects();
if ( isset( $objects[ $object_key ]['source_file'] ) ) {
$path = $this->prefix() . $objects[ $object_key ]['source_file'];
}
}
return $path;
}
/**
* Setter for item's path value.
*
* @param string $path
*/
public function set_path( $path ) {
$this->path = $path;
}
/**
* Getter for item's original_path value.
*
* @return string
*/
public function original_path() {
return $this->original_path;
}
/**
* Setter for item's original path value.
*
* @param string $path
*/
public function set_original_path( $path ) {
$this->original_path = $path;
}
/**
* Getter for item's is_private value.
*
* @param string|null $object_key
*
* @return bool
*/
public function is_private( $object_key = null ) {
if ( ! empty( $object_key ) ) {
$objects = $this->objects();
if ( isset( $objects[ $object_key ]['is_private'] ) ) {
return (bool) $objects[ $object_key ]['is_private'];
}
return false;
}
return (bool) $this->is_private;
}
/**
* Setter for item's is_private value
*
* @param bool $private
* @param string|null $object_key
*/
public function set_is_private( $private, $object_key = null ) {
if ( ! empty( $object_key ) ) {
$objects = $this->objects();
if ( isset( $objects[ $object_key ] ) ) {
$objects[ $object_key ]['is_private'] = $private;
$this->set_objects( $objects );
}
if ( $object_key === self::primary_object_key() ) {
$this->is_private = $private;
}
return;
}
$this->set_is_private( $private, self::primary_object_key() );
}
/**
* Any private objects in this item
*
* @return bool
*/
public function has_private_objects() {
foreach ( $this->objects() as $object ) {
if ( $object['is_private'] ) {
return true;
}
}
return false;
}
/**
* Getter for the item prefix
*
* @return string
*/
public function prefix() {
$dirname = dirname( $this->path );
$dirname = $dirname === '.' ? '' : $dirname;
return AS3CF_Utils::trailingslash_prefix( $dirname );
}
/**
* Get the private prefix for item's private objects.
*
* @return string
*/
public function private_prefix() {
$extra_info = $this->extra_info();
if ( ! empty( $extra_info['private_prefix'] ) ) {
return AS3CF_Utils::trailingslash_prefix( $extra_info['private_prefix'] );
}
return '';
}
/**
* Setter for the private prefix
*
* @param string $new_private_prefix
*/
public function set_private_prefix( $new_private_prefix ) {
$extra_info = $this->extra_info();
$extra_info['private_prefix'] = AS3CF_Utils::trailingslash_prefix( $new_private_prefix );
$this->set_extra_info( $extra_info );
}
/**
* Get the full remote key for this item including private prefix when needed
*
* @param string|null $object_key
*
* @return string
*/
public function provider_key( $object_key = null ) {
$path = $this->path( $object_key );
if ( $this->is_private( $object_key ) ) {
$path = $this->private_prefix() . $path;
}
return $path;
}
/**
* Returns an associative array of provider keys by their object_key.
*
* NOTE: There may be duplicate keys if object_keys reference same source file/object.
*
* @return array
*/
public function provider_keys() {
$keys = array();
foreach ( array_keys( $this->objects() ) as $object_key ) {
$keys[ $object_key ] = $this->provider_key( $object_key );
}
return $keys;
}
/**
* Creates a provider key for a given filename using the item's prefix settings.
*
* This function can be used to create ad-hoc custom provider keys.
* There are no tests to see if the filename is known to be associated with the item.
*
* @param string $filename Just a filename without any path.
* @param bool $is_private Should a private prefixed provider key be created if appropriate?
*
* @return string
*/
public function provider_key_for_filename( $filename, $is_private ) {
$provider_key = '';
if ( ! empty( $filename ) ) {
$provider_key = $this->prefix() . wp_basename( trim( $filename ) );
if ( $is_private ) {
$provider_key = $this->private_prefix() . $provider_key;
}
}
return $provider_key;
}
/**
* Getter for item's source_id value.
*
* @return integer
*/
public function source_id() {
return $this->source_id;
}
/**
* Getter for item's source_path value.
*
* @param string|null $object_key
*
* @return string
*/
public function source_path( $object_key = null ) {
if ( ! empty( $object_key ) ) {
$objects = $this->objects();
if ( isset( $objects[ $object_key ] ) ) {
$object_file = $objects[ $object_key ]['source_file'];
return str_replace( wp_basename( $this->source_path ), $object_file, $this->source_path );
}
}
return $this->source_path;
}
/**
* Setter for item's source_path value
*
* @param string $new_path
*/
public function set_source_path( $new_path ) {
$this->source_path = $new_path;
}
/**
* Getter for item's original_source_path value.
*
* @return string
*/
public function original_source_path() {
return $this->original_source_path;
}
/**
* Setter for item's original_source_path value
*
* @param string $new_path
*/
public function set_original_source_path( $new_path ) {
$this->original_source_path = $new_path;
}
/**
* Get an absolute source path.
*
* Default it is based on the WordPress uploads folder.
*
* @param string|null $object_key Optional, by default the original file's source path is used.
*
* @return string
*/
public function full_source_path( $object_key = null ) {
/**
* Filter the absolute directory path prefix for an item's source files.
*
* @param string $basedir Default is WordPress uploads folder.
* @param Item $as3cf_item The Item whose full source path is being accessed.
*/
$basedir = trailingslashit( apply_filters( 'as3cf_item_basedir', wp_upload_dir()['basedir'], $this ) );
return $basedir . $this->source_path( $object_key );
}
/**
* Creates an absolute source path for a given filename using the item's source path settings.
*
* This function can be used to create ad-hoc custom source file paths.
* There are no tests to see if the filename is known to be associated with the item.
*
* Default it is based on the WordPress uploads folder.
*
* @param string $filename Just a filename without any path.
*
* @return string
*/
public function full_source_path_for_filename( $filename ) {
if ( empty( $filename ) ) {
return '';
}
/**
* Filter the absolute directory path prefix for an item's source files.
*
* @param string $basedir Default is WordPress uploads folder.
* @param Item $as3cf_item The Item whose full source path is being accessed.
*/
$basedir = trailingslashit( apply_filters( 'as3cf_item_basedir', wp_upload_dir()['basedir'], $this ) );
return $basedir . str_replace( wp_basename( $this->source_path ), wp_basename( trim( $filename ) ), $this->source_path );
}
/**
* Getter for item's extra_info value.
*
* @return array
*/
public function extra_info() {
return $this->extra_info;
}
/**
* Setter for extra_info value.
*
* @param array $extra_info
*/
public function set_extra_info( $extra_info ) {
$this->extra_info = $extra_info;
}
/**
* Getter for item's originator value.
*
* @return integer
*/
public function originator() {
return $this->originator;
}
/**
* Setter for item's originator value.
*
* @param int $originator
*/
public function set_originator( $originator ) {
$this->originator = $originator;
}
/**
* Getter for item's is_verified value.
*
* @return bool
*/
public function is_verified() {
return (bool) $this->is_verified;
}
/**
* Setter for item's is_verified value.
*
* @param bool $is_verified
*/
public function set_is_verified( $is_verified ) {
$this->is_verified = (bool) $is_verified;
}
/**
* Does this item type use object versioning?
*
* @return bool
*/
public static function can_use_object_versioning() {
return static::CAN_USE_OBJECT_VERSIONING;
}
/**
* Get normalized object path dir.
*
* @return string
*/
public function normalized_path_dir() {
$directory = dirname( $this->path );
return ( '.' === $directory ) ? '' : AS3CF_Utils::trailingslash_prefix( $directory );
}
/**
* Get the first source id for a bucket and path.
*
* @param string $bucket
* @param string $path
*
* @return int|bool
*/
public static function get_source_id_by_bucket_and_path( $bucket, $path ) {
global $wpdb;
if ( empty( $bucket ) || empty( $path ) ) {
return false;
}
$item = static::get_from_items_cache_by_bucket_and_path( $bucket, $path );
if ( ! empty( $item ) ) {
return $item->source_id();
}
$sql = $wpdb->prepare(
"
SELECT source_id FROM " . static::items_table() . "
WHERE source_type = %s
AND bucket = %s
AND (path = %s OR original_path = %s)
ORDER BY source_id LIMIT 1
",
static::$source_type,
$bucket,
$path,
$path
);
$result = $wpdb->get_var( $sql );
return empty( $result ) ? false : (int) $result;
}
/**
* Get the source id for a given remote URL.
*
* @param string $url
*
* @return array|bool
*/
public static function get_item_source_by_remote_url( $url ) {
global $wpdb;
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
if ( ! AS3CF_Utils::usable_url( $url ) ) {
return false;
}
$parts = AS3CF_Utils::parse_url( $url );
$path = AS3CF_Utils::decode_filename_in_path( ltrim( $parts['path'], '/' ) );
// Remove the first directory to cater for bucket in path domain settings.
if ( false !== strpos( $path, '/' ) ) {
$path = explode( '/', $path );
array_shift( $path );
// If private prefix enabled, check if first segment and remove it as path/original_path do not include it.
// We can't check every possible private prefix as each item may have a unique private prefix.
// The only way to do that is with some fancy SQL, but that's not feasible as this particular
// SQL query is already troublesome on some sites with badly behaved themes/plugins.
if ( count( $path ) && $as3cf->get_delivery_provider()->use_signed_urls_key_file() ) {
// We have to be able to handle multi-segment private prefixes such as "private/downloads/".
$private_prefixes = explode( '/', untrailingslashit( $as3cf->get_setting( 'signed-urls-object-prefix' ) ) );
foreach ( $private_prefixes as $private_prefix ) {
if ( $private_prefix === $path[0] ) {
array_shift( $path );
} else {
// As soon as we don't have a match stop looking.
break;
}
}
}
$path = implode( '/', $path );
}
$sql = $wpdb->prepare(
"SELECT * FROM " . static::items_table() . " WHERE (path LIKE %s OR original_path LIKE %s);",
'%' . $path,
'%' . $path
);
$results = $wpdb->get_results( $sql );
// Nothing found, shortcut out.
if ( 0 === count( $results ) ) {
// TODO: If upgrade in progress, fallback to 'amazonS3_info' in Media_Library_Item override of this function.
return false;
}
// Regardless of whether 1 or many items found, must validate match.
$path = AS3CF_Utils::decode_filename_in_path( ltrim( $parts['path'], '/' ) );
foreach ( $results as $result ) {
/** @var Item $class */
$class = $as3cf->get_source_type_class( $result->source_type );
$as3cf_item = $class::create( $result );
// If item's bucket matches first segment of URL path, remove it from URL path before checking match.
if ( 0 === strpos( $path, trailingslashit( $as3cf_item->bucket() ) ) ) {
$match_path = ltrim( substr_replace( $path, '', 0, strlen( $as3cf_item->bucket() ) ), '/' );
} else {
$match_path = $path;
}
// If item's private prefix matches first segment of URL path, remove it from URL path before checking match.
if ( ! empty( $as3cf_item->private_prefix() ) && 0 === strpos( $match_path, $as3cf_item->private_prefix() ) ) {
$match_path = ltrim( substr_replace( $match_path, '', 0, strlen( $as3cf_item->private_prefix() ) ), '/' );
}
// Exact match, return ID.
if ( $as3cf_item->path() === $match_path || $as3cf_item->original_path() === $match_path ) {
return array(
'id' => $as3cf_item->source_id(),
'source_type' => $as3cf_item->source_type(),
);
}
}
return false;
}
/**
* Get an array of managed source_ids in descending order.
*
* While source id isn't strictly unique, it is by source type, which is always used in queries based on called class.
*
* @param int $upper_bound Returned source_ids should be lower than this, use null for no upper bound.
* @param int $limit Maximum number of source_ids to return. Required if not counting.
* @param bool $count Just return a count of matching source_ids? Negates $limit, default false.
* @param int $originator Optionally restrict to only records with given originator type from ORIGINATORS const.
* @param bool $is_verified Optionally restrict to only records that either are or are not verified.
*
* @return array|int
*/
public static function get_source_ids( $upper_bound, $limit, $count = false, $originator = null, $is_verified = null ) {
global $wpdb;
if ( $count ) {
$sql = 'SELECT COUNT(DISTINCT source_id)';
} else {
$sql = 'SELECT DISTINCT source_id';
}
$sql .= ' FROM ' . static::items_table() . ' WHERE source_type = %s';
$args = array( static::$source_type );
if ( is_numeric( $upper_bound ) ) {
$sql .= ' AND source_id < %d';
$args[] = $upper_bound;
}
// If an originator type given, check that it is valid before continuing and using.
if ( null !== $originator ) {
if ( is_int( $originator ) && in_array( $originator, self::ORIGINATORS ) ) {
$sql .= ' AND originator = %d';
$args[] = $originator;
} else {
AS3CF_Error::log( __METHOD__ . ' called with invalid originator: ' . $originator );
return $count ? 0 : array();
}
}
// If an is_verified value given, check that it is valid before continuing and using.
if ( null !== $is_verified ) {
if ( is_bool( $is_verified ) ) {
$sql .= ' AND is_verified = %d';
$args[] = (int) $is_verified;
} else {
AS3CF_Error::log( __METHOD__ . ' called with invalid is_verified: ' . $is_verified );
return $count ? 0 : array();
}
}
if ( ! $count ) {
$sql .= ' ORDER BY source_id DESC LIMIT %d';
$args[] = $limit;
}
$sql = $wpdb->prepare( $sql, $args );
if ( $count ) {
return $wpdb->get_var( $sql );
} else {
return array_map( 'intval', $wpdb->get_col( $sql ) );
}
}
/**
* Get an array of un-managed source_ids in descending order.
*
* While source id isn't strictly unique, it is by source type, which is always used in queries based on called class.
*
* @param int $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound.
* @param int $limit Maximum number of source_ids to return. Required if not counting.
* @param bool $count Just return a count of matching source_ids? Negates $limit, default false.
*
* @return array|int
*
* NOTE: Must be overridden by subclass, only reason this is not abstract is because static is preferred.
*/
public static function get_missing_source_ids( $upper_bound, $limit, $count = false ) {
if ( $count ) {
return 0;
} else {
return array();
}
}
/**
* Get array of objects (i.e. different sizes of same attachment item)
*
* @return array
*/
public function objects() {
$extra_info = $this->extra_info();
if ( isset( $extra_info['objects'] ) && is_array( $extra_info['objects'] ) ) {
// Make sure that the primary object key, if exists, comes first
$array_keys = array_keys( $extra_info['objects'] );
$primary_key = self::primary_object_key();
if ( in_array( $primary_key, $array_keys ) && $primary_key !== $array_keys[0] ) {
$extra_info['objects'] = array_merge( array( $primary_key => null ), $extra_info['objects'] );
}
return $extra_info['objects'];
}
return array();
}
/**
* Set array of objects (i.e. different sizes of same attachment item)
*
* @param array $objects
*/
public function set_objects( $objects ) {
$extra_info = $this->extra_info();
$extra_info['objects'] = $objects;
$this->set_extra_info( $extra_info );
}
/**
* Synthesize a data struct to be used when passing information
* about the current item to filters that assume the item is a
* media library item.
*
* @return array
*/
public function item_data_for_acl_filter() {
return array(
'source_type' => $this->source_type(),
'file' => $this->path( self::primary_object_key() ),
'sizes' => array_keys( $this->objects() ),
);
}
/**
* Get absolute source file paths for offloaded files.
*
* @return array Associative array of object_key => path
*/
abstract public function full_source_paths();
/**
* Get size name from file name.
*
* @param string $filename
*
* @return string
*/
abstract public function get_object_key_from_filename( $filename );
/**
* Get the provider URL for an item
*
* @param string|null $object_key
*
* @return string|false
*/
abstract public function get_local_url( $object_key = null );
/**
* Create a new item from the source id.
*
* @param int $source_id
* @param array $options
*
* @return Item|WP_Error
*/
public static function create_from_source_id( $source_id, $options = array() ) {
return new WP_Error(
'exception',
sprintf( 'Doing it wrong! Trying to create a base %s class instance from source ID %d', __CLASS__, $source_id )
);
}
/**
* Return a year/month string for the item
*
* @return string
*/
protected function get_item_time() {
return null;
}
/**
* Return an additional 'internal' prefix used by some item types
*
* @return string
*/
protected function get_internal_prefix() {
return '';
}
/**
* Get item's new public prefix path for current settings.
*
* @param bool $use_object_versioning
*
* @return string
*/
public function get_new_item_prefix( $use_object_versioning = true ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$prefix = $as3cf->get_object_prefix();
$time = $this->get_item_time();
$prefix .= AS3CF_Utils::trailingslash_prefix( $as3cf->get_dynamic_prefix( $time, static::$can_use_yearmonth ) );
if ( $use_object_versioning && static::can_use_object_versioning() && $as3cf->get_setting( 'object-versioning' ) ) {
$prefix .= AS3CF_Utils::trailingslash_prefix( $as3cf->get_object_version_string() );
}
return AS3CF_Utils::trailingslash_prefix( $prefix );
}
/**
* Get ACL for object key
*
* @param string $object_key Object key
* @param string|null $bucket Optional bucket that ACL is potentially to be used with.
*
* @return string|null
*/
public function get_acl_for_object_key( $object_key, $bucket = null ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$acl = null;
$use_acl = $as3cf->use_acl_for_intermediate_size( 0, $object_key, $bucket, $this );
if ( $use_acl ) {
$acl = $this->is_private( $object_key ) ? $as3cf->get_storage_provider_instance( $this->provider() )->get_private_acl() : $as3cf->get_storage_provider_instance( $this->provider() )->get_default_acl();
}
return $acl;
}
/**
* Search for all items that have the source path(s).
*
* @param array|string $paths Array of relative source paths.
* @param array|int $exclude_source_ids Array of source_ids to exclude from search. Default, none.
* @param bool $exact_match Use paths as supplied (true, default), or greedy match on path without extension (e.g. find edited too).
* @param bool $first_only Only return first matched item sorted by source_id. Default false.
*
* @return array
*/
public static function get_by_source_path( $paths, $exclude_source_ids = array(), $exact_match = true, $first_only = false ) {
global $wpdb;
if ( ! is_array( $paths ) && is_string( $paths ) && ! empty( $paths ) ) {
$paths = array( $paths );
}
if ( ! is_array( $paths ) || empty( $paths ) ) {
return array();
}
$paths = array_map( 'esc_sql', $paths );
$paths = AS3CF_Utils::make_upload_file_paths_relative( array_unique( $paths ) );
$sql = '
SELECT DISTINCT items.*
FROM ' . static::items_table() . ' AS items USE INDEX (uidx_source_path, uidx_original_source_path)
WHERE 1=1
';
if ( ! empty( $exclude_source_ids ) ) {
if ( ! is_array( $exclude_source_ids ) ) {
$exclude_source_ids = array( $exclude_source_ids );
}
$exclude_source_ids = array_map( 'intval', $exclude_source_ids );
$sql .= ' AND items.source_id NOT IN (' . join( ',', $exclude_source_ids ) . ')';
}
if ( $exact_match ) {
$sql .= " AND (items.source_path IN ('" . join( "','", $paths ) . "')";
$sql .= " OR items.original_source_path IN ('" . join( "','", $paths ) . "'))";
} else {
$likes = array_map( function ( $path ) {
$ext = '.' . pathinfo( $path, PATHINFO_EXTENSION );
$path = substr_replace( $path, '%', -strlen( $ext ) );
return "items.source_path LIKE '" . $path . "' OR items.original_source_path LIKE '" . $path . "'";
}, $paths );
$sql .= ' AND (' . join( ' OR ', $likes ) . ')';
}
if ( $first_only ) {
$sql .= ' ORDER BY items.source_id LIMIT 1';
}
return array_map( static::class . '::create', $wpdb->get_results( $sql ) );
}
/**
* Update path and original path with a new prefix
*
* @param string $new_prefix
*/
public function update_path_prefix( $new_prefix ) {
$this->set_path( $new_prefix . wp_basename( $this->path() ) );
$this->set_original_path( $new_prefix . wp_basename( $this->original_path() ) );
}
/**
* Returns a link to the items edit page in WordPress
*
* @param object $error
*
* @return object|null Null or object containing properties 'url' and 'text'
*/
public static function admin_link( $error ) {
return null;
}
/**
* Is the item served by provider.
*
* @param bool $skip_rewrite_check Still check if offloaded even if not currently rewriting URLs? Default: false
* @param bool $skip_current_provider_check Skip checking if offloaded to current provider. Default: false, negated if $provider supplied
* @param Storage_Provider|null $provider Provider where item is expected to be offloaded to. Default: currently configured provider
* @param bool $check_is_verified Check that metadata is verified, has no effect if $skip_rewrite_check is true. Default: false
*
* @return bool
*/
public function served_by_provider( $skip_rewrite_check = false, $skip_current_provider_check = false, Storage_Provider $provider = null, $check_is_verified = false ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
if ( ! $skip_rewrite_check && ! $as3cf->get_setting( 'serve-from-s3' ) ) {
// Not serving provider URLs
return false;
}
if ( ! $skip_rewrite_check && ! empty( $check_is_verified ) && ! $this->is_verified() ) {
// Offload not verified, treat as not offloaded.
return false;
}
if ( ! $skip_current_provider_check && empty( $provider ) ) {
$provider = $as3cf->get_storage_provider();
}
if ( ! empty( $provider ) && $provider::get_provider_key_name() !== $this->provider() ) {
// File not uploaded to required provider
return false;
}
return true;
}
/**
* Does the item's files exist locally?
*
* @return bool
*/
public function exists_locally() {
foreach ( $this->full_source_paths() as $path ) {
if ( file_exists( $path ) ) {
return true;
}
}
return false;
}
/**
* Get the provider URL for an item
*
* @param string|null $object_key
* @param int|null $expires
* @param array $headers
*
* @return string|WP_Error|bool
*/
public function get_provider_url( string $object_key = null, int $expires = null, array $headers = array() ) {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
if ( is_null( $object_key ) ) {
$object_key = self::primary_object_key();
}
// Is a signed expiring URL required for the requested object?
if ( is_null( $expires ) ) {
$expires = $this->is_private( $object_key ) ? Amazon_S3_And_CloudFront::DEFAULT_EXPIRES : null;
} else {
$expires = $this->is_private( $object_key ) ? $expires : null;
}
$scheme = $as3cf->get_url_scheme();
$enable_delivery_domain = $as3cf->get_delivery_provider()->delivery_domain_allowed() ? $as3cf->get_setting( 'enable-delivery-domain' ) : false;
$delivery_domain = $as3cf->get_setting( 'delivery-domain' );
$item_path = $this->path( $object_key );
if ( ! $enable_delivery_domain || empty( $delivery_domain ) ) {
$region = $this->region();
if ( is_wp_error( $region ) ) {
return $region;
}
$delivery_domain = $as3cf->get_storage_provider_instance( $this->provider() )->get_url_domain( $this->bucket(), $region, $expires );
} else {
$delivery_domain = AS3CF_Utils::sanitize_custom_domain( $delivery_domain );
}
if ( ! is_null( $expires ) && ! $as3cf->get_storage_provider()->needs_access_keys() ) {
try {
/**
* Filters the expires time for private content
*
* @param int $expires The expires time in seconds
*/
$timestamp = time() + apply_filters( 'as3cf_expires', $expires );
$url = $as3cf->get_delivery_provider()->get_signed_url( $this, $item_path, $delivery_domain, $scheme, $timestamp, $headers );
/**
* Filters the secure URL for private content
*
* @param string $url The URL
* @param Item $item The Item object
* @param array $item_source The item source descriptor array
* @param int $timestamp Expiry timestamp
* @param array $headers Optional extra http headers
*/
return apply_filters( 'as3cf_get_item_secure_url', $url, $this, $this->get_item_source_array(), $timestamp, $headers );
} catch ( Exception $e ) {
return new WP_Error( 'exception', $e->getMessage() );
}
} else {
try {
$url = $as3cf->get_delivery_provider()->get_url( $this, $item_path, $delivery_domain, $scheme, $headers );
/**
* Filters the URL for public content
*
* @param string $url The URL
* @param Item $item The Item object
* @param array $item_source The item source descriptor array
* @param int $source_id The source ID of the object
* @param int $timestamp Expiry timestamp
* @param array $headers Optional extra http headers
*/
return apply_filters( 'as3cf_get_item_url', $url, $this, $this->get_item_source_array(), $expires, $headers );
} catch ( Exception $e ) {
return new WP_Error( 'exception', $e->getMessage() );
}
}
}
/**
* Update file sizes after removing local files for an item
*
* @param int $original_size
* @param int $total_size
*/
public function update_filesize_after_remove_local( $original_size, $total_size ) {
}
/**
* Cleanup file sizes after getting item files back from the bucket
*/
public function update_filesize_after_download_local() {
}
/**
* If another item in current site shares full size *local* paths, only remove remote files not referenced by duplicates.
* We reference local paths as they should be reflected one way or another remotely, including backups.
*
* @param Item $as3cf_item
* @param array $paths
*/
public function remove_duplicate_paths( Item $as3cf_item, $paths ) {
return $paths;
}
/**
* Verify that the extra info uses the new format set in plugin version 2.6.0
* Update if needed
*
* @param array $extra_info
* @param int $source_id
* @param bool $is_private
*
* @since 2.6.0
*/
protected static function maybe_update_extra_info( &$extra_info, $source_id, $is_private ) {
if ( ! is_array( $extra_info ) ) {
$extra_info = array();
}
// Compatibility fallback for if just an array of private sizes is supplied.
$private_sizes = array();
if ( ! isset( $extra_info['private_sizes'] ) && ! isset( $extra_info['private_prefix'] ) && ! isset( $extra_info['objects'] ) ) {
$private_sizes = $extra_info;
}
// Compatibility fallback for old broken format.
if ( isset( $extra_info['private_sizes'] ) && isset( $extra_info['private_sizes']['private_sizes'] ) ) {
$extra_info['private_sizes'] = $extra_info['private_sizes']['private_sizes'];
}
// Extra info must have at least one element, if not it's broken.
if ( isset( $extra_info['objects'] ) && 0 === count( $extra_info['objects'] ) ) {
unset( $extra_info['objects'] );
}
if ( ! isset( $extra_info['objects'] ) ) {
$private_sizes = isset( $extra_info['private_sizes'] ) && is_array( $extra_info['private_sizes'] ) ? $extra_info['private_sizes'] : $private_sizes;
$extra_info['objects'] = array();
$files = AS3CF_Utils::get_attachment_file_paths( $source_id, false );
foreach ( $files as $object_key => $file ) {
if ( 'file' === $object_key ) {
continue;
}
$new_object = array(
'source_file' => wp_basename( $file ),
'is_private' => self::primary_object_key() === $object_key ? $is_private : in_array( $object_key, $private_sizes ),
);
$extra_info['objects'][ $object_key ] = $new_object;
}
}
if ( isset( $extra_info['private_sizes'] ) ) {
unset( $extra_info['private_sizes'] );
}
}
/**
* Returns the item source description array for this item
*
* @return array Array with the format:
* array(
* 'id' => 1,
* 'source_type' => 'foo-type',
* )
*/
public function get_item_source_array() {
return array(
'id' => $this->source_id(),
'source_type' => $this->source_type(),
);
}
/**
* Returns an array keyed by offloaded source file name.
*
* Each entry is as per objects, but also includes an array of object_keys.
*
* @return array
*/
public function offloaded_files() {
$offloaded_files = array();
foreach ( $this->objects() as $object_key => $object ) {
if ( isset( $offloaded_files[ $object['source_file'] ] ) ) {
$offloaded_files[ $object['source_file'] ]['object_keys'][] = $object_key;
} else {
$object['object_keys'] = array( $object_key );
$offloaded_files[ $object['source_file'] ] = $object;
}
}
return $offloaded_files;
}
/**
* Is the supplied item_source considered to be empty?
*
* @param array $item_source
*
* @return bool
*/
public static function is_empty_item_source( $item_source ) {
if (
empty( $item_source['source_type'] ) ||
! isset( $item_source['id'] ) ||
! is_numeric( $item_source['id'] ) ||
$item_source['id'] < 0
) {
return true;
}
return false;
}
/**
* Count items on current site.
*
* @param bool $skip_transient Whether to force database query and skip transient, default false
* @param bool $force Whether to force database query and skip static cache, implies $skip_transient, default false
* @param int $blog_id Optional, the blog ID to count media items for
*
* @return array Keys:
* total: Total media count for site (current blog id)
* offloaded: Count of offloaded media for site (current blog id)
* not_offloaded: Difference between total and offloaded
*/
public static function count_items( bool $skip_transient = false, bool $force = false, int $blog_id = 0 ): array {
if ( empty( $blog_id ) ) {
$blog_id = get_current_blog_id();
}
$transient_key = static::transient_key_for_item_counts( $blog_id );
// Been here, done it, won't do it again!
// Well, unless this is the first transient skip for the prefix, then we need to do it.
if ( ! $force && ! empty( static::$item_counts[ $transient_key ] ) && ( false === $skip_transient || ! empty( static::$item_count_skips[ $transient_key ] ) ) ) {
return static::$item_counts[ $transient_key ];
}
static $sites_count;
if ( $force || $skip_transient || false === ( $result = get_site_transient( $transient_key ) ) ) {
$result = static::get_item_counts();
ksort( $result );
// Timeout is randomised to ensure multisite subsites don't all try and update at the same time.
// Large site default of 15 - 120 minutes range gives us 6300 possible timeouts, checked every 5 minutes,
// with each subsite getting at least 15 mins breather before records counted again.
$min = 15;
$max = 120;
if ( empty( $sites_count ) ) {
$sites_count = is_multisite() ? count( AS3CF_Utils::get_blog_ids() ) : 1;
}
// For smaller media counts we can reduce the timeout to make changes more responsive
// without noticeably impacting performance.
if ( 5000 > $result['total'] && 50 > $sites_count ) {
$min = 0;
$max = 0;
} elseif ( 50000 > $result['total'] && 500 > $sites_count ) {
$min = 5;
$max = 15;
}
/**
* How many minutes minimum should a subsite's media counts be cached?
*
* Min: 0 minutes.
* Max: 1 day (1440 minutes).
*
* Default 0 for small media counts, 5 for medium (5k <= X < 50k), 15 for larger (>= 50k).
* However, on a multisite, 0 is only set for < 50 subsites, 5 for < 500 subsites, otherwise it's 15.
*
* @param int $minutes
* @param int $blog_id
* @param string $source_type The source type currently being counted, e.g. 'media-library'.
*
* @retun int
*/
$min = min( max( 0, (int) apply_filters( 'as3cf_blog_media_counts_timeout_min', $min, $blog_id, static::source_type() ) ), 1440 );
$max = max( $min, $max );
/**
* How many minutes maximum should a subsite's media counts be cached?
*
* Min: 0 minutes (or minimum set by as3cf_blog_media_counts_timeout_min filter for same blog id and source type).
* Max: 1 day (1440 minutes).
*
* Default 0 for small media counts, 15 for medium (5k <= X < 50k), 120 for larger (>= 50k).
* However, on a multisite, 0 is only set for < 50 subsites, 15 for < 500 subsites, otherwise it's 120.
*
* @param int $minutes Default or larger minimum set by as3cf_blog_media_counts_timeout_min filter for same blog id and source type.
* @param int $blog_id
* @param string $source_type The source type currently being counted, e.g. 'media-library'.
*
* @retun int
*/
$max = min( max( $min, (int) apply_filters( 'as3cf_blog_media_counts_timeout_max', $max, $blog_id, static::source_type() ) ), 1440 );
// We lied, our real minimums are min 3 and max 15 seconds
// to ensure there's at least a tiny bit of caching,
// which helps combat some potential race conditions,
// and makes sure the transient has a timeout.
$min = max( $min, 0.05 );
$max = max( $max, 0.25 );
set_site_transient( $transient_key, $result, rand( $min * MINUTE_IN_SECONDS, $max * MINUTE_IN_SECONDS ) );
// One way or another we've skipped the transient.
static::$item_count_skips[ $transient_key ] = true;
}
static::$item_counts[ $transient_key ] = $result;
return $result;
}
/**
* Returns the transient key to be used for storing blog specific item counts.
*
* @param int $blog_id
*
* @return string
*/
public static function transient_key_for_item_counts( int $blog_id ): string {
return 'as3cf_' . absint( $blog_id ) . '_attachment_counts_' . static::$source_type;
}
/**
* Count total, offloaded and not offloaded items on current site.
*
* @return array Keys:
* total: Total media count for site (current blog id)
* offloaded: Count of offloaded media for site (current blog id)
* not_offloaded: Difference between total and offloaded
*/
abstract protected static function get_item_counts(): array;
}