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
462 lines
12 KiB
PHP
462 lines
12 KiB
PHP
<?php
|
|
|
|
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
|
|
|
|
use Amazon_S3_And_CloudFront;
|
|
use AS3CF_Utils;
|
|
use DeliciousBrains\WP_Offload_Media\Items\Item;
|
|
use DirectoryIterator;
|
|
use WP_Error;
|
|
|
|
class BBoss_Item extends Item {
|
|
/**
|
|
* Buddy Boss images have random and unique names, object versioning not needed
|
|
*
|
|
* @var bool
|
|
*/
|
|
const CAN_USE_OBJECT_VERSIONING = false;
|
|
|
|
/**
|
|
* Item's summary type name.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $summary_type_name = 'BuddyBoss';
|
|
|
|
/**
|
|
* Item's summary type.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $summary_type = 'bboss';
|
|
|
|
/**
|
|
* The sprintf() pattern for creating prefix based on source_id.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $prefix_pattern = '';
|
|
|
|
/**
|
|
* Buddy Boss images are not managed in yearmonth folders
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected static $can_use_yearmonth = false;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $folder = '';
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected static $is_cover = false;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected static $is_group = false;
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
private static $chunk_size = 1000;
|
|
|
|
/**
|
|
* Get a Buddy Boss item object from the database
|
|
*
|
|
* @param int $source_id
|
|
* @param string $object_type
|
|
* @param string $image_type
|
|
*
|
|
* @return BBoss_Item|false
|
|
*/
|
|
public static function get_buddy_boss_item( $source_id, $object_type, $image_type ) {
|
|
/** @var BBoss_Item $class */
|
|
$class = static::get_item_class( $object_type, $image_type );
|
|
|
|
if ( ! empty( $class ) ) {
|
|
return $class::get_by_source_id( $source_id );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate Buddy Boss item sub class based on object and image type
|
|
*
|
|
* @param string $object_type user or group
|
|
* @param string $image_type avatar or cover image
|
|
*
|
|
* @return false|string
|
|
*/
|
|
public static function get_item_class( $object_type, $image_type ) {
|
|
$class_map = array(
|
|
'user' => array(
|
|
'avatar' => 'BBoss_User_Avatar',
|
|
'cover' => 'BBoss_User_Cover',
|
|
),
|
|
'group' => array(
|
|
'avatar' => 'BBoss_Group_Avatar',
|
|
'cover' => 'BBoss_Group_Cover',
|
|
),
|
|
);
|
|
|
|
if ( isset( $class_map[ $object_type ][ $image_type ] ) ) {
|
|
return __NAMESPACE__ . '\\' . $class_map[ $object_type ][ $image_type ];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new Buddy Boss item from the source id.
|
|
*
|
|
* @param int $source_id
|
|
* @param array $options
|
|
*
|
|
* @return BBoss_Item|WP_Error
|
|
*/
|
|
public static function create_from_source_id( $source_id, $options = array() ) {
|
|
$file_paths = static::get_local_files( $source_id );
|
|
if ( empty( $file_paths ) ) {
|
|
return new WP_Error(
|
|
'exception',
|
|
__( 'No local files found in ' . __FUNCTION__, 'amazon-s3-and-cloudfront' )
|
|
);
|
|
}
|
|
|
|
$file_path = static::remove_size_from_filename( $file_paths[ Item::primary_object_key() ] );
|
|
|
|
$extra_info = array( 'objects' => array() );
|
|
foreach ( $file_paths as $key => $path ) {
|
|
$extra_info['objects'][ $key ] = array(
|
|
'source_file' => wp_basename( $path ),
|
|
'is_private' => false,
|
|
);
|
|
}
|
|
|
|
return new static(
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
false,
|
|
$source_id,
|
|
$file_path,
|
|
null,
|
|
$extra_info,
|
|
self::CAN_USE_OBJECT_VERSIONING
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// Base prefix from settings
|
|
$prefix = $as3cf->get_object_prefix();
|
|
$prefix .= AS3CF_Utils::trailingslash_prefix( $as3cf->get_dynamic_prefix( null, static::$can_use_yearmonth ) );
|
|
|
|
// Buddy Boss specific prefix
|
|
$buddy_boss_prefix = sprintf( static::$prefix_pattern, $this->source_id() );
|
|
$prefix .= AS3CF_Utils::trailingslash_prefix( $buddy_boss_prefix );
|
|
|
|
return $prefix;
|
|
}
|
|
|
|
/**
|
|
* Return all buddy boss file sizes from the source folder
|
|
*
|
|
* @param int $source_id
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_local_files( $source_id ) {
|
|
$basedir = bp_core_get_upload_dir( 'upload_path' );
|
|
|
|
// Get base path and apply filters
|
|
if ( static::$is_cover ) {
|
|
// Call filters indirectly via bp_attachments_cover_image_upload_dir()
|
|
$args = array(
|
|
'object_id' => $source_id,
|
|
'object_directory' => str_replace( 'buddypress/', '', static::$folder ),
|
|
);
|
|
$upload_dir = bp_attachments_cover_image_upload_dir( $args );
|
|
$image_path = $upload_dir['path'];
|
|
} else {
|
|
// Call apply_filters directly
|
|
$image_path = trailingslashit( $basedir ) . trailingslashit( static::$folder ) . $source_id;
|
|
$object_type = static::$is_group ? 'group' : 'user';
|
|
$image_path = apply_filters(
|
|
'bp_core_avatar_folder_dir',
|
|
$image_path,
|
|
$source_id,
|
|
$object_type,
|
|
static::$folder
|
|
);
|
|
}
|
|
|
|
$result = array();
|
|
|
|
if ( ! file_exists( $image_path ) ) {
|
|
return $result;
|
|
}
|
|
|
|
$files = new DirectoryIterator( $image_path );
|
|
|
|
foreach ( $files as $file ) {
|
|
if ( $file->isDot() ) {
|
|
continue;
|
|
}
|
|
|
|
$base_name = $file->getFilename();
|
|
$file_name = substr( $file->getPathname(), strlen( $basedir ) );
|
|
$file_name = AS3CF_Utils::unleadingslashit( $file_name );
|
|
|
|
if ( false !== strpos( $base_name, '-bp-cover-image' ) ) {
|
|
$result[ Item::primary_object_key() ] = $file_name;
|
|
}
|
|
if ( false !== strpos( $base_name, '-bpfull' ) ) {
|
|
$result[ Item::primary_object_key() ] = $file_name;
|
|
}
|
|
if ( false !== strpos( $base_name, '-bpthumb' ) ) {
|
|
$result['thumb'] = $file_name;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Buddy Boss specific size removal from URL and convert it to a neutral
|
|
* (mock) file name with the correct file extension
|
|
*
|
|
* @param string $file_name The file
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function remove_size_from_filename( $file_name ) {
|
|
$path_info = pathinfo( $file_name );
|
|
|
|
return trailingslashit( $path_info['dirname'] ) . 'bp.' . $path_info['extension'];
|
|
}
|
|
|
|
/**
|
|
* Return size name based on the file name
|
|
*
|
|
* @param string $filename
|
|
*
|
|
* @return string | null
|
|
*/
|
|
public function get_object_key_from_filename( $filename ) {
|
|
return BuddyBoss::get_object_key_from_filename( $filename );
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public static function get_missing_source_ids( $upper_bound, $limit, $count = false ) {
|
|
global $wpdb;
|
|
|
|
// Bail out with empty values if we are a group class and the groups component is not active
|
|
if ( static::$is_group ) {
|
|
$active_bp_components = apply_filters( 'bp_active_components', bp_get_option( 'bp-active-components' ) );
|
|
if ( empty( $active_bp_components['groups'] ) ) {
|
|
return $count ? 0 : array();
|
|
}
|
|
}
|
|
|
|
$source_table = $wpdb->prefix . static::$source_table;
|
|
$basedir = bp_core_get_upload_dir( 'upload_path' );
|
|
$dir = trailingslashit( $basedir ) . static::$folder . '/';
|
|
$missing_items = array();
|
|
$missing_count = 0;
|
|
|
|
// Establish an upper bound if needed
|
|
if ( empty( $upper_bound ) ) {
|
|
$sql = "SELECT max(id) from $source_table";
|
|
$max_id = (int) $wpdb->get_var( $sql );
|
|
$upper_bound = $max_id + 1;
|
|
}
|
|
|
|
for ( $i = $upper_bound; $i >= 0; $i -= self::$chunk_size ) {
|
|
$args = array();
|
|
$sql = "
|
|
SELECT t.id as ID from $source_table as t
|
|
LEFT OUTER JOIN " . static::items_table() . " as i
|
|
ON (i.source_id = t.ID AND i.source_type=%s)";
|
|
$args[] = static::source_type();
|
|
|
|
$sql .= ' WHERE i.ID IS NULL AND t.id < %d';
|
|
$args[] = $upper_bound;
|
|
$sql .= ' ORDER BY t.ID DESC LIMIT %d, %d';
|
|
$args[] = $upper_bound - $i;
|
|
$args[] = self::$chunk_size;
|
|
$sql = $wpdb->prepare( $sql, $args );
|
|
|
|
$items_without_managed_offload = array_map( 'intval', $wpdb->get_col( $sql ) );
|
|
|
|
foreach ( $items_without_managed_offload as $item_without_managed_offload ) {
|
|
$target = $dir . $item_without_managed_offload;
|
|
if ( is_dir( $target ) ) {
|
|
if ( $count ) {
|
|
$missing_count++;
|
|
} else {
|
|
$missing_items[] = $item_without_managed_offload;
|
|
|
|
// If we have enough items, bail out
|
|
if ( count( $missing_items ) >= $limit ) {
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add custom default if available for offload.
|
|
if ( ( $count || count( $missing_items ) < $limit ) && is_dir( $dir . '0' ) ) {
|
|
if ( ! static::get_by_source_id( 0 ) && ! empty( static::get_local_files( 0 ) ) ) {
|
|
if ( $count ) {
|
|
$missing_count++;
|
|
} else {
|
|
$missing_items[] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $count ) {
|
|
return $missing_count;
|
|
} else {
|
|
return $missing_items;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setter for item's path & original path values
|
|
*
|
|
* @param string $path
|
|
*/
|
|
public function set_path( $path ) {
|
|
$path = static::remove_size_from_filename( $path );
|
|
parent::set_path( $path );
|
|
parent::set_original_path( $path );
|
|
}
|
|
|
|
/**
|
|
* Get absolute source file paths for offloaded files.
|
|
*
|
|
* @return array Associative array of object_key => path
|
|
*/
|
|
public function full_source_paths() {
|
|
$basedir = bp_core_get_upload_dir( 'upload_path' );
|
|
$item_folder = dirname( $this->source_path() );
|
|
|
|
$objects = $this->objects();
|
|
$sizes = array();
|
|
foreach ( $objects as $size => $object ) {
|
|
$sizes[ $size ] = trailingslashit( $basedir ) . trailingslashit( $item_folder ) . $object['source_file'];
|
|
}
|
|
|
|
return $sizes;
|
|
}
|
|
|
|
/**
|
|
* Get the local URL for an item
|
|
*
|
|
* @param string|null $object_key
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_local_url( $object_key = null ) {
|
|
if ( static::$is_cover ) {
|
|
return $this->get_local_cover_url( $object_key );
|
|
} else {
|
|
return $this->get_local_avatar_url( $object_key );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the local URL for an avatar item
|
|
*
|
|
* @param string|null $object_key
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_local_avatar_url( $object_key = null ) {
|
|
$uploads = wp_upload_dir();
|
|
$url = trailingslashit( $uploads['baseurl'] );
|
|
$url .= $this->source_path( $object_key );
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get the local URL for a cover item
|
|
*
|
|
* @param string|null $object_key
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_local_cover_url( $object_key = null ) {
|
|
$uploads = wp_upload_dir();
|
|
$url = trailingslashit( $uploads['baseurl'] );
|
|
$url .= $this->source_path( $object_key );
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get the prefix pattern
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_prefix_pattern() {
|
|
return static::$prefix_pattern;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
protected static function get_item_counts(): array {
|
|
global $wpdb;
|
|
|
|
$sql = 'SELECT count(id) FROM ' . static::items_table() . ' WHERE source_type = %s';
|
|
$sql = $wpdb->prepare( $sql, static::$source_type );
|
|
$offloaded_count = (int) $wpdb->get_var( $sql );
|
|
$missing_count = static::get_missing_source_ids( 0, 0, true );
|
|
|
|
if ( is_array( $missing_count ) ) {
|
|
$missing_count = count( $missing_count );
|
|
}
|
|
|
|
return array(
|
|
'total' => $offloaded_count + $missing_count,
|
|
'offloaded' => $offloaded_count,
|
|
'not_offloaded' => $missing_count,
|
|
);
|
|
}
|
|
}
|