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
This commit is contained in:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
class BBoss_Group_Avatar extends BBoss_Item {
/**
* Source type name
*
* @var string
*/
protected static $source_type_name = 'Buddy Boss Group Avatar';
/**
* Internal source type identifier
*
* @var string
*/
protected static $source_type = 'bboss-group-avatar';
/**
* Table (if any) that corresponds to this source type
*
* @var string
*/
protected static $source_table = 'bp_groups';
/**
* Foreign key (if any) in the $source_table
*
* @var string
*/
protected static $source_fk = 'id';
/**
* Relative folder where group avatars are stored on disk
*
* @var string
*/
protected static $folder = 'group-avatars';
/**
* The sprintf() pattern for creating prefix based on source_id.
*
* @var string
*/
protected static $prefix_pattern = 'group-avatars/%d';
/**
* @var bool
*/
protected static $is_group = true;
/**
* Returns a link to the items edit page in WordPress
*
* @param object $error
*
* @return object|null Object containing url and link text
*/
public static function admin_link( $error ) {
$url = self_admin_url( 'admin.php?page=bp-groups&action=edit&gid=' . $error->source_id );
if ( empty( $url ) ) {
return null;
}
return (object) array(
'url' => $url,
'text' => __( 'Edit', 'amazon-s3-and-cloudfront' ),
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
class BBoss_Group_Cover extends BBoss_Item {
/**
* Source type name
*
* @var string
*/
protected static $source_type_name = 'Buddy Boss Group Cover';
/**
* Internal source type identifier
*
* @var string
*/
protected static $source_type = 'bboss-group-cover';
/**
* Table (if any) that corresponds to this source type
*
* @var string
*/
protected static $source_table = 'bp_groups';
/**
* Foreign key (if any) in the $source_table
*
* @var string
*/
protected static $source_fk = 'id';
/**
* @var bool
*/
protected static $is_cover = true;
/**
* @var bool
*/
protected static $is_group = true;
/**
* Relative folder where group covers are stored on disk
*
* @var string
*/
protected static $folder = 'buddypress/groups';
/**
* The sprintf() pattern for creating prefix based on source_id.
*
* @var string
*/
protected static $prefix_pattern = 'buddypress/groups/%d/cover-image';
/**
* Returns a link to the items edit page in WordPress
*
* @param object $error
*
* @return object|null Object containing url and link text
*/
public static function admin_link( $error ) {
return BBoss_Group_Avatar::admin_link( $error );
}
}

View File

@@ -0,0 +1,461 @@
<?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,
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
class BBoss_User_Avatar extends BBoss_Item {
/**
* Source type name
*
* @var string
*/
protected static $source_type_name = 'Buddy Boss User Avatar';
/**
* Internal source type identifier
*
* @var string
*/
protected static $source_type = 'bboss-user-avatar';
/**
* Table (if any) that corresponds to this source type
*
* @var string
*/
protected static $source_table = 'users';
/**
* Foreign key (if any) in the $source_table
*
* @var string
*/
protected static $source_fk = 'id';
/**
* Relative folder where user avatars are stored on disk
*
* @var string
*/
protected static $folder = 'avatars';
/**
* The sprintf() pattern for creating prefix based on source_id.
*
* @var string
*/
protected static $prefix_pattern = 'avatars/%d';
/**
* Returns a link to the items edit page in WordPress
*
* @param object $error
*
* @return object|null Object containing url and link text
*/
public static function admin_link( $error ) {
$url = self_admin_url( 'users.php?page=bp-profile-edit&user_id=' . $error->source_id );
if ( empty( $url ) ) {
return null;
}
return (object) array(
'url' => $url,
'text' => __( 'Edit', 'amazon-s3-and-cloudfront' ),
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
class BBoss_User_Cover extends BBoss_Item {
/**
* Source type name
*
* @var string
*/
protected static $source_type_name = 'Buddy Boss User Cover';
/**
* Internal source type identifier
*
* @var string
*/
protected static $source_type = 'bboss-user-cover';
/**
* Table (if any) that corresponds to this source type
*
* @var string
*/
protected static $source_table = 'users';
/**
* Foreign key (if any) in the $source_table
*
* @var string
*/
protected static $source_fk = 'id';
/**
* @var bool
*/
protected static $is_cover = true;
/**
* Relative folder where user covers are stored on disk
*
* @var string
*/
protected static $folder = 'buddypress/members';
/**
* The sprintf() pattern for creating prefix based on source_id.
*
* @var string
*/
protected static $prefix_pattern = 'buddypress/members/%d/cover-image';
/**
* Returns a link to the items edit page in WordPress
*
* @param object $error
*
* @return object|null Object containing url and link text
*/
public static function admin_link( $error ) {
return BBoss_User_Avatar::admin_link( $error );
}
}

View File

@@ -0,0 +1,769 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\BuddyBoss;
use AS3CF_Error;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Items\Download_Handler;
use DeliciousBrains\WP_Offload_Media\Items\Item;
use DeliciousBrains\WP_Offload_Media\Items\Upload_Handler;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Remove_Provider_Handler;
use Exception;
class BuddyBoss extends Integration {
/**
* Our item types
*
* @var object[]
*/
private $source_types;
/**
* Are we inside a crop operation?
*
* @var bool
*/
private $in_crop = false;
/**
* Did we handle a crop operation?
*
* @var bool
*/
private $did_crop = false;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
global $buddyboss_platform_plugin_file;
if ( class_exists( 'BuddyPress' ) && is_string( $buddyboss_platform_plugin_file ) && ! is_multisite() ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
$this->source_types = array(
'bboss-user-avatar' => array(
'class' => BBoss_Item::get_item_class( 'user', 'avatar' ),
),
'bboss-user-cover' => array(
'class' => BBoss_Item::get_item_class( 'user', 'cover' ),
),
'bboss-group-avatar' => array(
'class' => BBoss_Item::get_item_class( 'group', 'avatar' ),
),
'bboss-group-cover' => array(
'class' => BBoss_Item::get_item_class( 'group', 'cover' ),
),
);
// Register our item source types with the global as3cf object.
foreach ( $this->source_types as $key => $source_type ) {
$this->as3cf->register_source_type( $key, $source_type['class'] );
}
// Register our item summary types with the global as3cf object.
$this->as3cf->register_summary_type( BBoss_Item::summary_type(), BBoss_Item::class );
}
/**
* @inheritDoc
*/
public function setup() {
// URL Rewriting.
add_filter( 'bp_core_fetch_avatar_url_check', array( $this, 'fetch_avatar' ), 10, 2 );
add_filter( 'bp_core_fetch_gravatar_url_check', array( $this, 'fetch_default_avatar' ), 99, 2 );
add_filter( 'bb_get_default_custom_upload_profile_avatar', array( $this, 'filter_bb_get_default_custom_upload_profile_avatar' ), 10, 2 );
add_filter( 'bb_get_default_custom_upload_group_avatar', array( $this, 'filter_bb_get_default_custom_upload_group_avatar' ), 10, 2 );
add_filter( 'bp_attachments_pre_get_attachment', array( $this, 'fetch_cover' ), 10, 2 );
add_filter( 'bb_get_default_custom_upload_profile_cover', array( $this, 'filter_bb_get_default_custom_upload_profile_cover' ), 10 );
add_filter( 'bb_get_default_custom_upload_group_cover', array( $this, 'filter_bb_get_default_custom_upload_group_cover' ), 10 );
// Storage handling.
add_action( 'bp_core_pre_avatar_handle_crop', array( $this, 'filter_bp_core_pre_avatar_handle_crop' ), 10, 2 );
add_action( 'xprofile_avatar_uploaded', array( $this, 'avatar_uploaded' ), 10, 3 );
add_action( 'groups_avatar_uploaded', array( $this, 'avatar_uploaded' ), 10, 3 );
add_action( 'xprofile_cover_image_uploaded', array( $this, 'user_cover_uploaded' ), 10, 1 );
add_action( 'groups_cover_image_uploaded', array( $this, 'groups_cover_uploaded' ), 10, 1 );
add_action( 'bp_core_delete_existing_avatar', array( $this, 'delete_existing_avatar' ), 10, 1 );
add_action( 'xprofile_cover_image_deleted', array( $this, 'delete_existing_user_cover' ), 10, 1 );
add_action( 'groups_cover_image_deleted', array( $this, 'delete_existing_group_cover' ), 10, 1 );
add_action( 'deleted_user', array( $this, 'handle_deleted_user' ), 10, 1 );
add_action( 'groups_delete_group', array( $this, 'groups_delete_group' ), 10, 1 );
add_filter( 'bp_attachments_pre_delete_file', array( $this, 'bp_attachments_pre_delete_file' ), 10, 2 );
// Internal filters.
add_filter( 'as3cf_remove_size_from_filename', array( $this, 'remove_size_from_filename' ), 10, 1 );
add_filter( 'as3cf_get_size_string_from_url_for_item_source', array( $this, 'get_size_string_from_url_for_item_source' ), 10, 3 );
add_filter( 'as3cf_get_provider_url_for_item_source', array( $this, 'filter_get_provider_url_for_item_source' ), 10, 3 );
add_filter( 'as3cf_get_local_url_for_item_source', array( $this, 'filter_get_local_url_for_item_source' ), 10, 3 );
add_filter( 'as3cf_strip_image_edit_suffix_and_extension', array( $this, 'filter_strip_image_edit_suffix_and_extension' ), 10, 2 );
}
/**
* If possible, rewrite local avatar URL to remote, possibly using substitute source.
*
* @param string $avatar_url
* @param array $params
* @param null|int $source_id Optional override for the source ID, e.g. default = 0.
*
* @return string
*/
private function rewrite_avatar_url( $avatar_url, $params, $source_id = null ) {
if ( ! $this->as3cf->get_setting( 'serve-from-s3' ) ) {
return $avatar_url;
}
if ( ! isset( $params['item_id'] ) || ! is_numeric( $params['item_id'] ) || empty( $params['object'] ) ) {
return $avatar_url;
}
if ( ! empty( $avatar_url ) && ! $this->as3cf->filter_local->url_needs_replacing( $avatar_url ) ) {
return $avatar_url;
}
if ( ! is_numeric( $source_id ) ) {
$source_id = $params['item_id'];
}
$as3cf_item = BBoss_Item::get_buddy_boss_item( $source_id, $params['object'], 'avatar' );
if ( false !== $as3cf_item ) {
$image_type = ! empty( $params['type'] ) ? $params['type'] : 'full';
$size = 'thumb' === $image_type ? 'thumb' : Item::primary_object_key();
$new_url = $as3cf_item->get_provider_url( $size );
if ( ! empty( $new_url ) && is_string( $new_url ) ) {
return $new_url;
}
}
return $avatar_url;
}
/**
* Returns the avatar's remote URL.
*
* @handles bp_core_fetch_avatar_url_check
*
* @param string $avatar_url
* @param array $params
*
* @return string
*/
public function fetch_avatar( $avatar_url, $params ) {
return $this->rewrite_avatar_url( $avatar_url, $params );
}
/**
* Returns the avatar's remote default URL if gravatar not supplied.
*
* @handles bp_core_fetch_gravatar_url_check
*
* @param string $avatar_url
* @param array $params
*
* @return string
*/
public function fetch_default_avatar( $avatar_url, $params ) {
return $this->rewrite_avatar_url( $avatar_url, $params, 0 );
}
/**
* Filters to change default custom upload avatar image.
*
* @handles bb_get_default_custom_upload_profile_avatar
*
* @param string $custom_upload_profile_avatar Default custom upload avatar URL.
* @param string $size This parameter specifies whether you'd like the 'full' or 'thumb' avatar.
*/
public function filter_bb_get_default_custom_upload_profile_avatar( $custom_upload_profile_avatar, $size ) {
$params = array(
'item_id' => 0,
'object' => 'user',
'type' => $size,
);
return $this->rewrite_avatar_url( $custom_upload_profile_avatar, $params );
}
/**
* Filters to change default custom upload avatar image.
*
* @handles bb_get_default_custom_upload_group_avatar
*
* @param string $custom_upload_group_avatar Default custom upload avatar URL.
* @param string $size This parameter specifies whether you'd like the 'full' or 'thumb' avatar.
*/
public function filter_bb_get_default_custom_upload_group_avatar( $custom_upload_group_avatar, $size ) {
$params = array(
'item_id' => 0,
'object' => 'group',
'type' => $size,
);
return $this->rewrite_avatar_url( $custom_upload_group_avatar, $params );
}
/**
* If possible, rewrite local cover URL to remote, possibly using substitute source.
*
* @param string $cover_url
* @param array $params
* @param null|int $source_id Optional override for the source ID, e.g. default = 0.
*
* @return string
*/
private function rewrite_cover_url( $cover_url, $params, $source_id = null ) {
if ( ! $this->as3cf->get_setting( 'serve-from-s3' ) ) {
return $cover_url;
}
if ( ! isset( $params['item_id'] ) || ! is_numeric( $params['item_id'] ) || empty( $params['object_dir'] ) ) {
return $cover_url;
}
$object_type = $this->object_type_from_dir( $params['object_dir'] );
if ( is_null( $object_type ) ) {
return $cover_url;
}
if ( ! empty( $cover_url ) && ! $this->as3cf->filter_local->url_needs_replacing( $cover_url ) ) {
return $cover_url;
}
if ( ! is_numeric( $source_id ) ) {
$source_id = $params['item_id'];
}
$as3cf_item = BBoss_Item::get_buddy_boss_item( $source_id, $object_type, 'cover' );
if ( false !== $as3cf_item ) {
$new_url = $as3cf_item->get_provider_url( Item::primary_object_key() );
if ( ! empty( $new_url ) && is_string( $new_url ) ) {
// We should not supply remote URL during a delete operation,
// but the delete process will fail if there isn't a local file to delete.
if ( isset( $_POST['action'] ) && 'bp_cover_image_delete' === $_POST['action'] ) {
if ( ! $as3cf_item->exists_locally() ) {
/** @var Download_Handler $download_handler */
$download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() );
$download_handler->handle( $as3cf_item );
}
return $cover_url;
}
return $new_url;
}
}
return $cover_url;
}
/**
* Returns the cover's remote URL.
*
* @handles bp_attachments_pre_get_attachment
*
* @param string $cover_url
* @param array $params
*
* @return string
*/
public function fetch_cover( $cover_url, $params ) {
return $this->rewrite_cover_url( $cover_url, $params );
}
/**
* Filters to change default custom upload cover image.
*
* @handles bb_get_default_custom_upload_profile_cover
*
* @param string $value Default custom upload profile cover URL.
*/
public function filter_bb_get_default_custom_upload_profile_cover( $value ) {
$params = array(
'item_id' => 0,
'object_dir' => 'members',
);
return $this->rewrite_cover_url( $value, $params );
}
/**
* Filters default custom upload cover image URL.
*
* @handles bb_get_default_custom_upload_group_cover
*
* @param string $value Default custom upload group cover URL.
*/
public function filter_bb_get_default_custom_upload_group_cover( $value ) {
$params = array(
'item_id' => 0,
'object_dir' => 'groups',
);
return $this->rewrite_cover_url( $value, $params );
}
/**
* Filters whether or not to handle cropping.
*
* But we use it to catch a successful crop so we can offload
* and later supply the correct remote URL.
*
* @handles bp_core_pre_avatar_handle_crop
*
* @param bool $value Whether or not to crop.
* @param array $r Array of parsed arguments for function.
*
* @throws Exception
*/
public function filter_bp_core_pre_avatar_handle_crop( $value, $r ) {
if ( ! function_exists( 'bp_core_avatar_handle_crop' ) ) {
return $value;
}
$this->in_crop = ! $this->in_crop;
if ( $this->in_crop ) {
if ( bp_core_avatar_handle_crop( $r ) ) {
$this->avatar_uploaded( $r['item_id'], 'crop', $r );
$this->did_crop = true;
}
// We handled the crop.
return false;
}
// Don't cancel operation when we call it above.
return $value;
}
/**
* Handle a newly uploaded avatar
*
* @handles xprofile_avatar_uploaded
* @handles groups_avatar_uploaded
*
* @param int $source_id
* @param string $avatar_type
* @param array $params
*
* @throws Exception
*/
public function avatar_uploaded( $source_id, $avatar_type, $params ) {
if ( $this->did_crop ) {
return;
}
if ( ! $this->as3cf->get_setting( 'copy-to-s3' ) ) {
return;
}
if ( empty( $params['object'] ) ) {
return;
}
$object_type = $params['object'];
$image_type = 'avatar';
$as3cf_item = BBoss_Item::get_buddy_boss_item( $source_id, $object_type, $image_type );
if ( false !== $as3cf_item ) {
$this->delete_existing_avatar( array( 'item_id' => $source_id, 'object' => $object_type ) );
}
/** @var BBoss_Item $class */
$class = BBoss_Item::get_item_class( $object_type, $image_type );
$as3cf_item = $class::create_from_source_id( $source_id );
$upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() );
$upload_result = $upload_handler->handle( $as3cf_item );
// TODO: Should not be needed ...
if ( is_wp_error( $upload_result ) ) {
return;
}
// TODO: ... as this should be redundant.
// TODO: However, when user has offloaded avatar and replaces it,
// TODO: this save is required as handler returns false.
// TODO: As there is a delete above, this should not be the case!
$as3cf_item->save();
}
/**
* Handle when a new user cover image is uploaded
*
* @handles xprofile_cover_image_uploaded
*
* @param int $source_id
*
* @throws Exception
*/
public function user_cover_uploaded( $source_id ) {
$this->cover_uploaded( $source_id, 'user' );
}
/**
* Handle when a new group cover image is uploaded
*
* @handles xprofile_cover_image_uploaded
*
* @param int $source_id
*
* @throws Exception
*/
public function groups_cover_uploaded( $source_id ) {
$this->cover_uploaded( $source_id, 'group' );
}
/**
* Handle a new group or user cover image
*
* @param int $source_id
* @param string $object_type
*
* @throws Exception
*/
private function cover_uploaded( $source_id, $object_type ) {
if ( ! $this->as3cf->get_setting( 'copy-to-s3' ) ) {
return;
}
$as3cf_item = BBoss_Item::get_buddy_boss_item( $source_id, $object_type, 'cover' );
if ( false !== $as3cf_item ) {
$this->delete_existing_cover( $source_id, $object_type );
}
/** @var BBoss_Item $class */
$class = BBoss_Item::get_item_class( $object_type, 'cover' );
$as3cf_item = $class::create_from_source_id( $source_id );
$upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() );
$upload_handler->handle( $as3cf_item );
}
/**
* Removes a user cover image from the remote bucket
*
* @handles xprofile_cover_image_deleted
*
* @param int $source_id
*/
public function delete_existing_user_cover( $source_id ) {
$this->delete_existing_cover( $source_id, 'user' );
}
/**
* Removes a group cover image from the remote bucket
*
* @handles groups_cover_image_deleted
*
* @param int $source_id
*/
public function delete_existing_group_cover( $source_id ) {
$this->delete_existing_cover( $source_id, 'group' );
}
/**
* Removes a cover image from the remote bucket
*
* @handles bp_core_delete_existing_avatar
*
* @param int $source_id
* @param string $object_type
*/
public function delete_existing_cover( $source_id, $object_type ) {
/** @var BBoss_Item $as3cf_item */
$as3cf_item = BBoss_Item::get_buddy_boss_item( $source_id, $object_type, 'cover' );
if ( ! empty( $as3cf_item ) ) {
$remove_provider = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() );
$remove_provider->handle( $as3cf_item, array( 'verify_exists_on_local' => false ) );
$as3cf_item->delete();
}
}
/**
* Removes avatar and cover from remote bucket when a user is deleted
*
* @handles deleted_user
*
* @param int $user_id
*/
public function handle_deleted_user( $user_id ) {
$args = array( 'item_id' => $user_id, 'object' => 'user' );
$this->delete_existing_avatar( $args );
$this->delete_existing_cover( $user_id, 'user' );
}
/**
* Removes avatar and cover when a group is deleted
*
* @handles groups_delete_group
*
* @param int $group_id
*/
public function groups_delete_group( $group_id ) {
$args = array( 'item_id' => $group_id, 'object' => 'group' );
$this->delete_existing_avatar( $args );
$this->delete_existing_cover( $group_id, 'group' );
}
/**
* Removes an avatar from the remote bucket
*
* @handles bp_core_delete_existing_avatar
*
* @param array $args
*/
public function delete_existing_avatar( $args ) {
if ( ! isset( $args['item_id'] ) || ! is_numeric( $args['item_id'] ) || empty( $args['object'] ) ) {
return;
}
/** @var BBoss_Item $as3cf_item */
$as3cf_item = BBoss_Item::get_buddy_boss_item( $args['item_id'], $args['object'], 'avatar' );
if ( ! empty( $as3cf_item ) ) {
$remove_provider = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() );
$remove_provider->handle( $as3cf_item, array( 'verify_exists_on_local' => false ) );
$as3cf_item->delete();
}
}
/**
* Identifies URLs to avatars and cover images and rewrites the URL to
* the size neutral version.
*
* @handles as3cf_remove_size_from_filename
*
* @param string $url
*
* @return string
*/
public function remove_size_from_filename( $url ) {
$found_match = false;
foreach ( $this->source_types as $source_type ) {
/** @var BBoss_Item $class */
$class = $source_type['class'];
$pattern = sprintf(
'/\/%s\/[0-9a-f]{9,14}-bp(full|thumb|\-cover\-image)\./',
str_replace( '%d', '\d+', preg_quote( $class::get_prefix_pattern(), '/' ) )
);
$match = preg_match( $pattern, $url );
if ( ! empty( $match ) ) {
$found_match = true;
break;
}
}
if ( ! $found_match ) {
return $url;
}
return BBoss_Item::remove_size_from_filename( $url );
}
/**
* Get the size from a URL for the Buddy Boss item types
*
* @handles as3cf_get_size_string_from_url_for_item_source
*
* @param string $size
* @param string $url
* @param array $item_source
*
* @return string
*/
public function get_size_string_from_url_for_item_source( $size, $url, $item_source ) {
if ( ! in_array( $item_source['source_type'], array_keys( $this->source_types ), true ) ) {
return $size;
}
return static::get_object_key_from_filename( $url );
}
/**
* Return size name based on the file name
*
* @param string $filename
*
* @return string | null
*/
public static function get_object_key_from_filename( $filename ) {
$size = Item::primary_object_key();
$filename = preg_replace( '/\?.*/', '', $filename );
$basename = AS3CF_Utils::encode_filename_in_path( wp_basename( $filename ) );
if ( false !== strpos( $basename, '-bpthumb' ) ) {
$size = 'thumb';
}
return $size;
}
/**
* Get the remote URL for a User / Group avatar or cover image
*
* @handles as3cf_get_provider_url_for_item_source
*
* @param string $url Url
* @param array $item_source The item source descriptor array
* @param string $size Name of requested size
*
* @return string
*/
public function filter_get_provider_url_for_item_source( $url, $item_source, $size ) {
if ( Item::is_empty_item_source( $item_source ) ) {
return $url;
}
if ( ! in_array( $item_source['source_type'], array_keys( $this->source_types ), true ) ) {
return $url;
}
/** @var BBoss_Item $class */
$class = ! empty( $this->source_types[ $item_source['source_type'] ] ) ? $this->source_types[ $item_source['source_type'] ]['class'] : false;
if ( false !== $class ) {
if ( empty( $size ) ) {
$size = Item::primary_object_key();
}
$as3cf_item = $class::get_by_source_id( $item_source['id'] );
if ( empty( $as3cf_item ) || ! $as3cf_item->served_by_provider() ) {
return $url;
}
$url = $as3cf_item->get_provider_url( $size );
}
return $url;
}
/**
* Get the local URL for a User / Group avatar or cover image
*
* @handles as3cf_get_local_url_for_item_source
*
* @param string $url Url
* @param array $item_source The item source descriptor array
* @param string $size Name of requested size
*
* @return string
*/
public function filter_get_local_url_for_item_source( $url, $item_source, $size ) {
if ( Item::is_empty_item_source( $item_source ) ) {
return $url;
}
if ( ! in_array( $item_source['source_type'], array_keys( $this->source_types ), true ) ) {
return $url;
}
/** @var BBoss_Item $class */
$class = ! empty( $this->source_types[ $item_source['source_type'] ] ) ? $this->source_types[ $item_source['source_type'] ]['class'] : false;
if ( ! empty( $class ) ) {
if ( empty( $size ) ) {
$size = Item::primary_object_key();
}
$as3cf_item = $class::get_by_source_id( $item_source['id'] );
if ( empty( $as3cf_item ) ) {
return $url;
}
$url = $as3cf_item->get_local_url( $size );
}
return $url;
}
/**
* Remove fake filename ending from a stripped bucket key
*
* @handles as3cf_strip_image_edit_suffix_and_extension
*
* @param string $path
* @param string $source_type
*
* @return string
*/
public function filter_strip_image_edit_suffix_and_extension( $path, $source_type ) {
if ( ! in_array( $source_type, array_keys( $this->source_types ), true ) ) {
return $path;
}
if ( '/bp' === substr( $path, -3 ) ) {
$path = trailingslashit( dirname( $path ) );
}
return $path;
}
/**
* Handle / override Buddy Boss attempt to delete a local file that we have already removed
*
* @handles bp_attachments_pre_delete_file
*
* @param bool $pre
* @param array $args
*
* @return bool
*/
public function bp_attachments_pre_delete_file( $pre, $args ) {
if ( empty( $args['object_dir'] ) || empty( $args['item_id'] ) ) {
return $pre;
}
$object_type = $this->object_type_from_dir( $args['object_dir'] );
if ( is_null( $object_type ) ) {
return $pre;
}
/** @var BBoss_Item $class */
$class = BBoss_Item::get_item_class( $object_type, 'cover' );
$as3cf_item = $class::get_by_source_id( (int) $args['item_id'] );
if ( ! $as3cf_item ) {
return $pre;
}
$source_file = $as3cf_item->full_source_path( Item::primary_object_key() );
if ( file_exists( $source_file ) ) {
return $pre;
}
return false;
}
/**
* Return object_type (user or group) based on object_dir passed in from Buddy Boss
*
* @param string $object_dir
*
* @return string|null
*/
private function object_type_from_dir( $object_dir ) {
switch ( $object_dir ) {
case 'members':
return 'user';
case 'groups':
return 'group';
}
AS3CF_Error::log( 'Unknown object_dir ' . $object_dir );
return null;
}
}