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; }