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,59 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\API\V1;
use DeliciousBrains\WP_Offload_Media\Pro\API\API;
use DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\Domain_Check_Response;
use WP_REST_Request;
use WP_REST_Response;
class Assets_Domain_Check extends API {
/** @var int */
protected static $version = 1;
/** @var string */
protected static $name = 'assets-domain-check';
/**
* Register REST API routes.
*/
public function register_routes() {
register_rest_route(
static::api_namespace(),
static::route() . '(?P<key>[\w\d=]+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_domain_check' ),
'permission_callback' => '__return_true', // public access
)
);
}
/**
* Respond to a GET request to the domain check route, with the given key.
*
* @param WP_REST_Request $request
*
* @return WP_REST_Response
*/
public function get_domain_check( WP_REST_Request $request ): WP_REST_Response {
$response = new Domain_Check_Response( array(
'key' => $request->get_param( 'key' ),
'ver' => filter_input( INPUT_GET, 'ver' ), // must come in as url param
) );
$response->header( 'X-As3cf-Signature', $response->hashed_signature() );
return $this->rest_ensure_response( 'get', static::name(), $response );
}
/**
* Get a URL to the domain check route, with the given key.
*
* @param string $key
*
* @return string
*/
public function get_url( string $key ): string {
return rest_url( static::endpoint() . $key );
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\API\V1;
use DeliciousBrains\WP_Offload_Media\API\V1\Settings;
use DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\Assets;
class Assets_Settings extends Settings {
/** @var int */
protected static $version = 1;
/** @var string */
protected static $name = 'assets-settings';
/**
* Common response values for this API endpoint.
*
* @return array
*/
public function common_response(): array {
/** @var Assets|null */
$assets = $this->as3cf->get_integration_manager()->get_integration( 'assets' );
return array(
'assets_settings' => $assets->obfuscate_sensitive_settings( $assets->get_all_settings() ),
'assets_defined_settings' => array_keys( $assets->get_defined_settings() ),
);
}
/**
* Handle saving settings submitted by user.
*
* @param array $new_settings
*
* @return array
*/
protected function save_settings( array $new_settings ): array {
$changed_keys = array();
do_action( 'as3cf_pre_save_assets_settings' );
/** @var Assets $assets */
$assets = $this->as3cf->get_integration_manager()->get_integration( 'assets' );
$allowed = $assets->get_allowed_settings_keys();
$old_settings = $assets->get_all_settings( false );
// Merge in defined settings as they take precedence and must overwrite anything supplied.
// Only needed to allow for validation, during save defined settings are removed from array anyway.
$new_settings = array_merge( $new_settings, $assets->get_defined_settings() );
foreach ( $allowed as $key ) {
// Whether defined or not, get rid of old database setting for key.
$assets->remove_setting( $key );
if ( ! isset( $new_settings[ $key ] ) ) {
continue;
}
$value = $assets->sanitize_setting( $key, $new_settings[ $key ] );
$assets->set_setting( $key, $value );
if ( $this->setting_changed( $old_settings, $key, $value ) ) {
$changed_keys[] = $key;
}
}
// Great success ...
$assets->save_settings();
do_action( 'as3cf_post_save_assets_settings', true );
return $changed_keys;
}
}

View File

@@ -0,0 +1,669 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets;
use Amazon_S3_And_CloudFront_Pro;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\API\V1\State;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\API\V1\Assets_Domain_Check;
use DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets\API\V1\Assets_Settings;
use DeliciousBrains\WP_Offload_Media\Settings\Domain_Check;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Domain_Check_Exception;
use DeliciousBrains\WP_Offload_Media\Settings\Validator_Interface;
use DeliciousBrains\WP_Offload_Media\Settings_Interface;
use DeliciousBrains\WP_Offload_Media\Settings_Trait;
use DeliciousBrains\WP_Offload_Media\Settings_Validator_Trait;
use Exception;
use WP_Error as AS3CF_Result;
class Assets extends Integration implements Settings_Interface, Validator_Interface {
use Settings_Trait;
use Settings_Validator_Trait;
const SETTINGS_KEY = 'as3cf_assets_pull';
const VALIDATOR_KEY = 'assets';
/**
* @var Amazon_S3_And_CloudFront_Pro
*/
protected $as3cf;
/**
* Cache property of whether integration is enabled.
*
* @var bool
*/
private $enabled;
/**
* @var array
*/
protected static $settings_constants = array(
'AS3CF_ASSETS_PULL_SETTINGS',
'WPOS3_ASSETS_PULL_SETTINGS',
);
/**
* @inheritDoc
*/
public static function is_installed(): bool {
return true;
}
/**
* Is this integration enabled?
*
* @return bool
*/
public function is_enabled(): bool {
if ( is_null( $this->enabled ) ) {
if ( parent::is_enabled() && $this->as3cf->feature_enabled( 'assets' ) ) {
$this->enabled = true;
} else {
$this->enabled = false;
}
}
return $this->enabled;
}
/**
* @inheritDoc
*/
public function init() {
// UI - Common
add_filter( 'as3cfpro_js_strings', array( $this, 'add_js_strings' ) );
// Don't enable the remaining hooks unless integration enabled.
if ( ! $this->is_enabled() ) {
return;
}
// UI - Enabled
add_filter( 'as3cfpro_js_config', array( $this, 'add_js_config' ) );
add_filter( 'as3cf_get_docs', array( $this, 'get_docs' ) );
// REST-API
add_filter( 'as3cf_api_endpoints', array( $this, 'add_api_endpoints' ) );
add_filter(
$this->as3cf->get_plugin_prefix() . '_api_response_get_' . State::name(),
array( $this, 'api_get_state' )
);
// Support
add_filter( 'as3cf_diagnostic_info', array( $this, 'diagnostic_info' ) );
// Keep track of whether the settings we're responsible for are currently being saved.
add_action( 'as3cf_pre_save_assets_settings', function () {
$this->set_saving_settings( true );
} );
add_action( 'as3cf_post_save_assets_settings', function () {
$this->set_saving_settings( false );
} );
// Register this instance as a validator.
$this->as3cf->validation_manager->register_validator( self::VALIDATOR_KEY, $this );
}
/**
* @inheritDoc
*/
public function setup() {
// Don't enable the hooks unless integration and URL rewriting enabled.
if ( ! $this->is_enabled() || ! $this->should_rewrite_urls() ) {
return;
}
// URL Rewriting.
add_filter( 'style_loader_src', array( $this, 'rewrite_src' ), 10, 2 );
add_filter( 'script_loader_src', array( $this, 'rewrite_src' ), 10, 2 );
add_filter( 'as3cf_get_asset', array( $this, 'rewrite_src' ) );
add_filter( 'wp_resource_hints', array( $this, 'register_resource_hints' ), 10, 2 );
}
/*
* Settings Interface
*/
/**
* Accessor for a plugin setting with conditions for defaults and upgrades.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function get_setting( string $key, $default = '' ) {
$settings = $this->get_settings();
$value = isset( $settings[ $key ] ) ? $settings[ $key ] : $default;
if ( 'rewrite-urls' == $key && ! isset( $settings[ $key ] ) ) {
$value = false;
}
if ( 'force-https' == $key && ! isset( $settings[ $key ] ) ) {
$value = false;
}
return apply_filters( 'as3cf_assets_pull_setting_' . $key, $value );
}
/**
* Allowed settings keys for this plugin.
*
* @param bool $include_legacy Should legacy keys be included? Optional, default false.
*
* @return array
*/
public function get_allowed_settings_keys( bool $include_legacy = false ): array {
return array(
'rewrite-urls',
'domain',
'force-https',
);
}
/**
* @inheritDoc
*/
public function get_sensitive_settings(): array {
return array();
}
/**
* @inheritDoc
*/
public function get_monitored_settings_blacklist(): array {
return array();
}
/**
* @inheritDoc
*/
public function get_skip_sanitize_settings(): array {
return array();
}
/**
* @inheritDoc
*/
public function get_path_format_settings(): array {
return array();
}
/**
* @inheritDoc
*/
public function get_prefix_format_settings(): array {
return array();
}
/**
* @inheritDoc
*/
public function get_boolean_format_settings(): array {
return array(
'rewrite-urls',
'force-https',
);
}
/*
* UI
*/
/**
* Add additional translated strings for integration.
*
* @param array $strings
*
* @return array
*/
public function add_js_strings( array $strings ): array {
return array_merge( $strings, array(
'assets_panel_header' => _x( 'Assets Pull', 'Assets panel title', 'amazon-s3-and-cloudfront' ),
'assets_panel_header_details' => _x( 'Deliver scripts, styles, fonts and other assets from a content delivery network.', 'Assets panel details', 'amazon-s3-and-cloudfront' ),
'assets_rewrite_urls' => _x( 'Rewrite Asset URLs', 'Setting title', 'amazon-s3-and-cloudfront' ),
'assets_rewrite_urls_desc' => _x( 'Change the URLs of any enqueued asset files to use a CDN domain.', 'Setting description', 'amazon-s3-and-cloudfront' ),
'assets_force_https' => _x( 'Force HTTPS', 'Setting title', 'amazon-s3-and-cloudfront' ),
'assets_force_https_desc' => _x( 'Uses HTTPS for every rewritten asset URL instead of using the scheme of the current page.', 'Setting description', 'amazon-s3-and-cloudfront' ),
'assets_domain_same_as_site' => __( "Domain cannot be the same as the site's domain; use a subdomain instead.", 'amazon-s3-and-cloudfront' ),
) );
}
/**
* Add additional config for integration.
*
* @param array $config
*
* @return array
*/
public function add_js_config( array $config ): array {
return array_merge(
$config,
$this->as3cf->get_api_manager()->get_api_endpoint(
Assets_Settings::name()
)->common_response()
);
}
/**
* Add doc links for this integration.
*
* @handles as3cf_docs_data
*
* @param array $docs_data
*
* @return array
*/
public function get_docs( array $docs_data ): array {
$docs_data['assets-pull'] = array(
'url' => $this->as3cf::dbrains_url( '/wp-offload-media/doc/assets-quick-start-guide', array( 'utm_campaign' => 'WP+Offload+S3', 'assets+doc' => 'assets-tab' ) ),
'desc' => _x( 'Click to view help doc on our site', 'Help icon alt text', 'amazon-s3-and-cloudfront' ),
);
return $docs_data;
}
/*
* REST-API
*/
/**
* Add API endpoints.
*
* @handles as3cf_api_endpoints
*
* @param array $api_endpoints
*
* @return array
*/
public function add_api_endpoints( array $api_endpoints ): array {
return array_merge( $api_endpoints, array(
Assets_Domain_Check::name() => new Assets_Domain_Check( $this->as3cf ),
Assets_Settings::name() => new Assets_Settings( $this->as3cf ),
) );
}
/**
* Add additional config into state API responses.
*
* @param array $response
*
* @return array
*/
public function api_get_state( array $response ): array {
return array_merge(
$response,
$this->as3cf->get_api_manager()->get_api_endpoint(
Assets_Settings::name()
)->common_response()
);
}
/*
* Support
*/
/**
* Integration specific diagnostic info.
*
* @param string $output
*
* @return string
*/
public function diagnostic_info( string $output = '' ): string {
$output .= 'Assets Pull:';
$output .= "\r\n";
$output .= 'Rewrite URLs: ';
$output .= $this->on_off( 'rewrite-urls' );
$output .= "\r\n";
$output .= 'Domain: ';
$output .= esc_html( $this->get_setting( 'domain' ) );
$output .= "\r\n";
$output .= 'Force HTTPS: ';
$output .= $this->on_off( 'force-https' );
$output .= "\r\n";
$output .= 'Domain Check: ';
$output .= $this->diagnostic_domain_check();
$output .= "\r\n\r\n";
$output .= 'AS3CF_ASSETS_PULL_SETTINGS: ';
$settings_constant = static::settings_constant();
if ( $settings_constant ) {
$output .= 'Defined';
if ( 'AS3CF_ASSETS_PULL_SETTINGS' !== $settings_constant ) {
$output .= ' (using ' . $settings_constant . ')';
}
$defined_settings = $this->get_defined_settings();
if ( empty( $defined_settings ) ) {
$output .= ' - *EMPTY*';
} else {
$output .= "\r\n";
$output .= 'AS3CF_ASSETS_PULL_SETTINGS Keys: ' . implode( ', ', array_keys( $defined_settings ) );
}
} else {
$output .= 'Not defined';
}
$output .= "\r\n";
return $output;
}
/**
* Run a domain check on the configured domain for the diagnostic information.
*
* @return string
*/
protected function diagnostic_domain_check(): string {
$domain = $this->get_setting( 'domain' );
if ( empty( $domain ) ) {
return '(no domain)';
}
$check = new Domain_Check( $domain );
try {
$this->run_domain_check( $check );
} catch ( Exception $e ) {
return $e->getMessage();
}
return 'OK';
}
/*
* Domain Check
*/
/**
* Check given domain is configured correctly.
*
* @param string $domain
*
* @return array
*/
public function check_domain( string $domain ): array {
$check = new Domain_Check( $domain );
try {
$this->run_domain_check( $check );
} catch ( Exception $e ) {
return array(
'success' => false,
'domain' => $check->domain(),
'message' => _x( 'Assets cannot be delivered from the CDN.', 'Assets domain check error', 'amazon-s3-and-cloudfront' ),
'error' => $e->getMessage(),
'link' => $this->domain_check_more_info_link( $e ),
'timestamp' => current_time( 'timestamp' ),
);
}
return array(
'success' => true,
'domain' => $check->domain(),
'message' => _x( 'Assets are serving from the CDN with the configured domain name.', 'Assets domain check success for active domain', 'amazon-s3-and-cloudfront' ),
'timestamp' => current_time( 'timestamp' ),
);
}
/**
* Build a "More info" link for a domain check error message.
*
* @param string|Exception $message
*
* @return string
*/
protected function domain_check_more_info_link( $message ): string {
$exception = $message;
if ( $message instanceof Exception ) {
$message = $exception->getMessage();
}
$utm_content = 'assets+domain+check';
if ( $exception instanceof Domain_Check_Exception ) {
return $this->as3cf->more_info_link( $exception->more_info(), $utm_content, $exception->get_key() );
}
// Fall-back to a search of the docs.
return $this->as3cf->more_info_link( '/wp-offload-media/docs/?swpquery=' . urlencode( $message ), $utm_content );
}
/**
* Execute the given domain check.
*
* @param Domain_Check $check
*
* @throws Exception
*/
protected function run_domain_check( Domain_Check $check ) {
$test_time = microtime();
$test_key = base64_encode( $test_time ); //phpcs:ignore
$this->test_assets_endpoint( $check, $test_key, $test_time );
}
/**
* Send a request to the test endpoint and make assertions about the response.
*
* @param Domain_Check $check
* @param string $key
* @param string $ver
*
* @throws Exception
*/
protected function test_assets_endpoint( Domain_Check $check, string $key, string $ver ) {
/** @var Assets_Domain_Check $domain_check */
$domain_check = $this->as3cf->get_api_manager()->get_api_endpoint(
Assets_Domain_Check::name()
);
$test_endpoint = $domain_check->get_url( $key );
$test_endpoint = add_query_arg( compact( 'ver' ), $test_endpoint );
$test_endpoint = $this->rewrite_url( $test_endpoint );
$response = $check->test_rest_endpoint( $test_endpoint );
$expected = new Domain_Check_Response( compact( 'key', 'ver' ) );
$expected->verify_signature( wp_remote_retrieve_header( $response, 'x-as3cf-signature' ) );
}
/*
* URL Rewriting
*/
/**
* Should asset URL rewriting be performed?
*
* @return bool
*/
public function should_rewrite_urls(): bool {
// TODO: cache result and reuse.
if ( ! $this->get_setting( 'rewrite-urls' ) ) {
return false;
}
if (
! Domain_Check::is_valid( $this->get_setting( 'domain' ) ) ||
$this->as3cf->validation_manager->section_has_error( self::VALIDATOR_KEY )
) {
return false;
}
if ( is_admin() && ! AS3CF_Utils::is_ajax() && ! AS3CF_Utils::is_rest_api() ) {
/**
* If you're really brave, you can have Assets Pull also rewrite enqueued assets
* within the WordPress admin dashboard.
*
* @param bool $rewrite
*/
return apply_filters( 'as3cf_assets_enable_wp_admin_rewrite', false );
}
return true;
}
/**
* Should the given asset URL be rewritten?
*
* @param mixed $src The asset URL to be rewritten.
* @param string|null $handle The asset's registered handle in the WordPress enqueue system.
*
* @return bool
*/
public static function should_rewrite_src( $src, string $handle = null ): bool {
// If there is no string to rewrite, the answer is definitely no.
if ( empty( $src ) || ! is_string( $src ) ) {
return false;
}
if ( AS3CF_Utils::is_relative_url( $src ) ) {
$rewrite = true;
} elseif ( AS3CF_Utils::url_domains_match( $src, home_url() ) ) {
$rewrite = true;
} else {
$rewrite = false;
}
/**
* @param bool $rewrite Should the src be rewritten?
* @param string $src The asset URL to be rewritten.
* @param string|null $handle The asset's registered handle in the WordPress enqueue system.
*/
return apply_filters( 'as3cf_assets_should_rewrite_src', $rewrite, $src, $handle );
}
/**
* Rewrite an asset's src.
*
* @param mixed $src
* @param string|null $handle
*
* @return mixed
*/
public function rewrite_src( $src, string $handle = null ) {
if ( empty( $src ) || ! is_string( $src ) ) {
return $src;
}
if ( ! $this->should_rewrite_urls() ) {
return $src;
}
if ( ! static::should_rewrite_src( $src, $handle ) ) {
return $src;
}
return $this->rewrite_url( $src, $handle );
}
/**
* Rewrite a URL to use the asset's domain and scheme.
*
* @param string $url
* @param string|null $handle
*
* @return string
*/
protected function rewrite_url( string $url, string $handle = null ): string {
$rewritten = 'http://' . $this->get_setting( 'domain' );
$rewritten .= AS3CF_Utils::parse_url( $url, PHP_URL_PATH );
$query = AS3CF_Utils::parse_url( $url, PHP_URL_QUERY );
$rewritten .= $query ? ( '?' . $query ) : '';
$scheme = $this->get_setting( 'force-https' ) ? 'https' : null;
/**
* Adjust the URL scheme that Assets Pull is going to use for rewritten URL.
*
* @param string|null $scheme
* @param string $url
* @param string $handle
*/
$scheme = apply_filters( 'as3cf_assets_pull_scheme', $scheme, $url, $handle );
return set_url_scheme( $rewritten, $scheme );
}
/**
* Register a DNS prefetch tag for the pull domain if rewriting is enabled.
*
* @param array $hints
* @param string $relation_type
*
* @return array
*/
public function register_resource_hints( array $hints, string $relation_type ): array {
if ( 'dns-prefetch' === $relation_type && $this->should_rewrite_urls() ) {
$hints[] = '//' . $this->get_setting( 'domain' );
}
return $hints;
}
/**
* Validate settings for Assets.
*
* @param bool $force Force time resource consuming or state altering tests to run.
*
* @return AS3CF_Result
*/
public function validate_settings( bool $force = false ): AS3CF_Result {
if ( $this->get_setting( 'rewrite-urls' ) ) {
$domain_check_result = $this->check_domain( $this->get_setting( 'domain' ) );
// Did the domain check fail?
if ( ! $domain_check_result['success'] ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
sprintf(
_x( '%1$s %2$s In the meantime local assets are being served. %3$s', 'Assets notice for domain issue', 'amazon-s3-and-cloudfront' ),
$domain_check_result['message'],
$domain_check_result['error'],
$domain_check_result['link']
)
);
}
// All good.
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS,
$domain_check_result['message']
);
} else {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
sprintf(
__(
'Assets cannot be delivered from the CDN until <strong>Rewrite Asset URLs</strong> is enabled. In the meantime, local assets are being served. <a href="%1$s" target="_blank">View Assets Quick Start Guide</a>',
'amazon-s3-and-cloudfront'
),
$this->as3cf::dbrains_url( '/wp-offload-media/doc/assets-quick-start-guide', array( 'utm_campaign' => 'WP+Offload+S3', 'assets+doc' => 'assets-tab' ) )
)
);
}
}
/**
* Get the name of the actions that are fired when the settings that the validator
* is responsible for are saved.
*
* @return array
*/
public function post_save_settings_actions(): array {
return array( 'as3cf_post_save_assets_settings' );
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations\Assets;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Settings\Exceptions\Signature_Verification_Exception;
use WP_REST_Response;
class Domain_Check_Response extends WP_REST_Response {
/**
* Verify that this response is valid for the given hashed signature.
*
* @param string $signature A hashed signature to verify this response against.
*
* @throws Signature_Verification_Exception
*/
public function verify_signature( string $signature ) {
if ( ! wp_check_password( $this->raw_signature(), $signature ) ) {
throw new Signature_Verification_Exception(
__( 'Invalid request signature.', 'amazon-s3-and-cloudfront' )
);
}
}
/**
* Get the hashed signature for this response.
*
* @return string
*/
public function hashed_signature(): string {
return wp_hash_password( $this->raw_signature() );
}
/**
* Get the raw signature for this response.
*
* @return string
*/
protected function raw_signature(): string {
return AS3CF_Utils::reduce_url( network_home_url() ) . '|' . json_encode( $this->jsonSerialize() ) . '|' . AUTH_SALT;
}
}

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

View File

@@ -0,0 +1,52 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use Amazon_S3_And_CloudFront_Pro;
use DeliciousBrains\WP_Offload_Media\Integrations\Core;
use DeliciousBrains\WP_Offload_Media\Items\Download_Handler;
use DeliciousBrains\WP_Offload_Media\Items\Item;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Remove_Provider_Handler;
use WP_Error;
class Core_Pro extends Core {
/**
* @var Amazon_S3_And_CloudFront_Pro
*/
protected $as3cf;
/**
* @inheritDoc
*/
public function setup() {
parent::setup();
add_action( 'as3cf_pre_handle_item_' . Remove_Provider_Handler::get_item_handler_key_name(), array( $this, 'maybe_download_files' ), 10, 3 );
}
/**
* Before removing from provider, maybe download files.
*
* @handles as3cf_pre_handle_item_remove-provider
*
* @param bool $cancel Should the action on the item be cancelled?
* @param Item $as3cf_item The item that the action is being handled for.
* @param array $options Handler dependent options that may have been set for the action.
*
* @return bool|WP_Error
*/
public function maybe_download_files( $cancel, Item $as3cf_item, array $options ) {
if ( false === $cancel && ! empty( $options['verify_exists_on_local'] ) ) {
$download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() );
$result = $download_handler->handle( $as3cf_item );
// If there was any kind of error, then remove from provider should not proceed.
// Because this is an unexpected error, bubble it.
if ( is_wp_error( $result ) ) {
return $result;
}
}
return $cancel;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
class Divi extends Integration {
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
// This integration fixes problems introduced by Divi Page Builder as used by the Divi and related themes.
if ( defined( 'ET_BUILDER_VERSION' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
// Nothing to do.
}
/**
* @inheritDoc
*/
public function setup() {
add_filter( 'et_fb_load_raw_post_content', function ( $content ) {
return apply_filters( 'as3cf_filter_post_local_to_provider', $content );
} );
// Before attachment lookup via GUID, revert remote URL to local URL.
add_filter( 'et_get_attachment_id_by_url_guid', function ( $url ) {
return apply_filters( 'as3cf_filter_post_provider_to_local', $url );
} );
// Global Modules reset their filtered background image URLs, so let's fix that.
if ( defined( 'ET_BUILDER_LAYOUT_POST_TYPE' ) ) {
add_filter( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
}
// The Divi Page Builder Gallery uses a non-standard and inherently anti-filter method of getting its editor thumbnails.
if ( $this->doing_fetch_attachments() ) {
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
}
// The Divi Theme Builder may need to refresh its cached CSS if media URLs possibly changed.
if ( function_exists( 'et_core_page_resource_auto_clear' ) ) {
add_action( 'as3cf_copy_buckets_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_copy_buckets_completed', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_download_and_remover_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_download_and_remover_completed', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_objects_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_objects_completed', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_private_objects_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_private_objects_completed', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_public_objects_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_move_public_objects_completed', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_uploader_cancelled', 'et_core_page_resource_auto_clear' );
add_action( 'as3cf_uploader_completed', 'et_core_page_resource_auto_clear' );
}
}
/**
* Is current request an et_fb_fetch_attachments AJAX call?
*
* @return bool
*/
private function doing_fetch_attachments() {
if ( AS3CF_Utils::is_ajax() && ! empty( $_POST['action'] ) && 'et_fb_fetch_attachments' === $_POST['action'] ) {
return true;
}
return false;
}
/**
* Turn filtering on for WP_Query calls initiated by the et_fb_fetch_attachments AJAX call.
*
* @param \WP_Query $query
*/
public function pre_get_posts( \WP_Query $query ) {
if ( ! empty( $query->query['post_type'] ) && 'attachment' === $query->query['post_type'] ) {
$query->query_vars['suppress_filters'] = false;
}
}
/**
* Handler for the 'the_posts' filter that runs local to provider URL filtering on Divi pages.
*
* @param array|\WP_Post $posts
* @param \WP_Query $query
*
* @return array
*/
public function the_posts( $posts, $query ) {
if (
defined( 'ET_BUILDER_LAYOUT_POST_TYPE' ) &&
! empty( $posts ) &&
! empty( $query ) &&
is_a( $query, 'WP_Query' ) &&
! empty( $query->query_vars['post_type'] )
) {
if ( is_array( $posts ) ) {
foreach ( $posts as $idx => $post ) {
$posts[ $idx ] = $this->the_posts( $post, $query );
}
} elseif ( is_a( $posts, 'WP_Post' ) ) {
$content_field = 'post_content';
if ( $this->doing_fetch_attachments() && 'attachment' === $posts->post_type ) {
$content_field = 'guid';
}
$posts->{$content_field} = apply_filters( 'as3cf_filter_post_local_to_provider', $posts->{$content_field} );
}
}
return $posts;
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use Amazon_S3_And_CloudFront_Pro;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Update_Acl_Handler;
use Exception;
use WP_Error;
class Easy_Digital_Downloads extends Integration {
/**
* @var Amazon_S3_And_CloudFront_Pro
*/
protected $as3cf;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'Easy_Digital_Downloads' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
// Nothing to do.
}
/**
* @inheritDoc
*/
public function setup() {
// Set download method to redirect
add_filter( 'edd_file_download_method', array( $this, 'set_download_method' ) );
// Disable using symlinks for download.
add_filter( 'edd_symlink_file_downloads', array( $this, 'disable_symlink_file_downloads' ) );
// Hook into edd_requested_file to swap in the S3 secure URL
add_filter( 'edd_requested_file', array( $this, 'get_download_url' ), 10, 3 );
// Hook into the save download files metabox to apply the private ACL
add_filter( 'edd_metabox_save_edd_download_files', array( $this, 'make_edd_files_private_on_provider' ), 11 );
}
/**
* Set download method
*
* @param string $method
*
* @return string
*/
public function set_download_method( $method ) {
return 'redirect';
}
/**
* Disable symlink file downloads
*
* @param bool $use_symlink
*
* @return bool
*/
public function disable_symlink_file_downloads( $use_symlink ) {
return false;
}
/**
* Uses the secure S3 url for downloads of a file
*
* @param string $file
* @param array $download_files
* @param string $file_key
*
* @return bool|string|WP_Error
* @throws Exception
*
* @handles edd_requested_file
*/
public function get_download_url( $file, $download_files, $file_key ) {
global $edd_options;
if ( empty( $file ) || empty( $download_files ) || empty( $file_key ) || ! is_array( $download_files ) || empty( $download_files[ $file_key ] ) ) {
return $file;
}
$file_data = $download_files[ $file_key ];
$file_name = $file_data['file'];
$post_id = $file_data['attachment_id'];
$expires = apply_filters( 'as3cf_edd_download_expires', 5 );
$headers = apply_filters( 'as3cf_edd_download_headers', array(
'ResponseContentDisposition' => 'attachment',
), $file_data );
// Standard Offloaded Media Library Item.
$as3cf_item = Media_Library_Item::get_by_source_id( $post_id );
if ( $as3cf_item && ! is_wp_error( $as3cf_item ) ) {
return $as3cf_item->get_provider_url( null, $expires, $headers );
}
// Official EDD S3 addon upload - path should not start with '/', 'http', 'https' or 'ftp' or contain AWSAccessKeyId
$url = parse_url( $file_name );
if ( ( '/' !== $file_name[0] && false === isset( $url['scheme'] ) ) || false !== ( strpos( $file_name, 'AWSAccessKeyId' ) ) ) {
$bucket = ( isset( $edd_options['edd_amazon_s3_bucket'] ) ) ? trim( $edd_options['edd_amazon_s3_bucket'] ) : $this->as3cf->get_setting( 'bucket' );
$expires = time() + $expires;
return $this->as3cf->get_provider_client()->get_object_url( $bucket, $file_name, $expires, $headers );
}
// None S3 upload
return $file;
}
/**
* Apply ACL to files uploaded outside of EDD on save of EDD download files
*
* @param array $files
*
* @return mixed
*/
public function make_edd_files_private_on_provider( $files ) {
global $post;
// get existing files attached to download
$old_files = edd_get_download_files( $post->ID );
$old_attachment_ids = wp_list_pluck( $old_files, 'attachment_id' );
$new_attachment_ids = array();
/** @var Update_Acl_Handler $acl_handler */
$acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() );
if ( is_array( $files ) ) {
foreach ( $files as $file ) {
$new_attachment_ids[] = $file['attachment_id'];
$as3cf_item = Media_Library_Item::get_by_source_id( $file['attachment_id'] );
if ( ! $as3cf_item ) {
// not offloaded, ignore.
continue;
}
if ( $as3cf_item->is_private() ) {
// already private
continue;
}
if ( $this->as3cf->is_pro_plugin_setup( true ) ) {
$options = array(
'object_keys' => array( null ),
'set_private' => true,
);
$result = $acl_handler->handle( $as3cf_item, $options );
if ( true === $result ) {
$this->as3cf->make_acl_admin_notice( $as3cf_item );
}
}
}
}
// determine which attachments have been removed and maybe set to public
$removed_attachment_ids = array_diff( $old_attachment_ids, $new_attachment_ids );
$this->maybe_make_removed_edd_files_public( $removed_attachment_ids, $post->ID );
return $files;
}
/**
* Remove public ACL from attachments removed from a download
* as long as they are not attached to any other downloads
*
* @param array $attachment_ids
* @param integer $download_id
*/
function maybe_make_removed_edd_files_public( $attachment_ids, $download_id ) {
global $wpdb;
/** @var Update_Acl_Handler $acl_handler */
$acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() );
foreach ( $attachment_ids as $id ) {
$as3cf_item = Media_Library_Item::get_by_source_id( $id );
if ( ! $as3cf_item ) {
// Not offloaded, ignore.
continue;
}
if ( ! $as3cf_item->is_private() ) {
// already public
continue;
}
$length = strlen( $id );
// check the attachment isn't used by other downloads
$sql = "
SELECT COUNT(*)
FROM `{$wpdb->prefix}postmeta`
WHERE `{$wpdb->prefix}postmeta`.`meta_key` = 'edd_download_files'
AND `{$wpdb->prefix}postmeta`.`post_id` != {$download_id}
AND `{$wpdb->prefix}postmeta`.`meta_value` LIKE '%s:13:\"attachment_id\";s:{$length}:\"{$id}\"%'
";
if ( $wpdb->get_var( $sql ) > 0 ) {
// used for another download, ignore
continue;
}
// set acl to public
$options = array(
'object_keys' => array( null ),
'set_private' => false,
);
$result = $acl_handler->handle( $as3cf_item, $options );
if ( true === $result ) {
$this->as3cf->make_acl_admin_notice( $as3cf_item );
}
}
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use Elementor\Core\Files\CSS\Post;
use Elementor\Element_Base;
use Elementor\Plugin;
use AS3CF_Utils;
class Elementor extends Integration {
/**
* Keep track update_metadata recursion level
*
* @var int
*/
private $recursion_level = 0;
/**
* Is Elementor installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( defined( 'ELEMENTOR_VERSION' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
// Nothing to do.
}
/**
* @inheritDoc
*/
public function setup() {
add_filter( 'elementor/editor/localize_settings', array( $this, 'localize_settings' ) );
add_action( 'elementor/frontend/before_render', array( $this, 'frontend_before_render' ) );
add_filter( 'update_post_metadata', array( $this, 'update_post_metadata' ), 10, 5 );
add_action( 'wp_print_styles', array( $this, 'wp_print_styles' ) );
if ( isset( $_REQUEST['action'] ) && 'elementor_ajax' === $_REQUEST['action'] ) {
add_filter( 'widget_update_callback', array( $this, 'widget_update_callback' ), 10, 2 );
}
// Hooks to clear Elementor cache after OME bulk actions
add_action( 'as3cf_copy_buckets_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_copy_buckets_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_download_and_remover_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_download_and_remover_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_objects_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_objects_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_private_objects_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_private_objects_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_public_objects_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_move_public_objects_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_uploader_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_uploader_completed', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_elementor_analyze_and_repair_cancelled', array( $this, 'clear_elementor_css_cache' ) );
add_action( 'as3cf_elementor_analyze_and_repair_completed', array( $this, 'clear_elementor_css_cache' ) );
}
/**
* Rewrite media library URLs from local to remote when settings are read from
* database.
*
* @param object $config
*
* @return object
*
* @handles elementor/editor/localize_settings
*/
public function localize_settings( $config ) {
if ( ! is_array( $config ) || ! isset( $config['initial_document'] ) ) {
return $config;
}
if ( ! is_array( $config['initial_document'] ) || ! isset( $config['initial_document']['elements'] ) ) {
return $config;
}
$filtered = json_decode(
$this->as3cf->filter_local->filter_post( json_encode( $config['initial_document']['elements'], JSON_UNESCAPED_SLASHES ) ),
true
);
// Avoid replacing content if the filtering failed
if ( false !== $filtered ) {
$config['initial_document']['elements'] = $filtered;
}
return $config;
}
/**
* Replace local URLs in settings that Elementor renders in HTML for some attributes, i.e json structs for
* the section background slideshow
*
* @param Element_Base $element
*
* @handles elementor/frontend/before_render
*/
public function frontend_before_render( $element ) {
$element->set_settings(
json_decode(
$this->as3cf->filter_local->filter_post( json_encode( $element->get_settings(), JSON_UNESCAPED_SLASHES ) ),
true
)
);
}
/**
* Handle Elementor's call to update_metadata() for _elementor_data when saving
* a post or page. Rewrites remote URLs to local.
*
* @param bool $check
* @param int $object_id
* @param string $meta_key
* @param mixed $meta_value
* @param mixed $prev_value
*
* @handles update_post_metadata
*
* @return bool
*/
public function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
if ( '_elementor_css' === $meta_key ) {
$this->rewrite_css( $object_id, $meta_value );
return $check;
}
if ( '_elementor_data' !== $meta_key ) {
return $check;
}
// We expect the meta value to be a JSON formatted string from Elementor. Exit early if it's not.
if ( ! is_string( $meta_value ) || ! AS3CF_Utils::is_json( $meta_value ) ) {
return $check;
}
// We're calling update_metadata recursively and need to make sure
// we never nest deeper than one level.
if ( 0 === $this->recursion_level ) {
$this->recursion_level++;
// We get here from an update_metadata() call that has already done some string sanitizing
// including wp_unslash(), but the original json from Elementor still needs slashes
// removed for our filters to work.
// Note: wp_unslash can't be used because it also unescapes any embedded HTML.
$json = str_replace( '\/', '/', $meta_value );
$json = $this->as3cf->filter_provider->filter_post( $json );
$meta_value = wp_slash( str_replace( '/', '\/', $json ) );
update_metadata( 'post', $object_id, '_elementor_data', $meta_value, $prev_value );
// Reset recursion level and let update_metadata we already handled saving the meta
$this->recursion_level = 0;
return true;
}
return $check;
}
/**
* Rewrites local URLs in generated CSS files
*
* @param int $object_id
* @param array $meta_value
*/
private function rewrite_css( $object_id, $meta_value ) {
if ( 'file' === $meta_value['status'] ) {
$elementor_css = new Post( $object_id );
$file = Post::get_base_uploads_dir() . Post::DEFAULT_FILES_DIR . $elementor_css->get_file_name();
if ( file_exists( $file ) ) {
$old_content = file_get_contents( $file );
if ( ! empty( $old_content ) ) {
file_put_contents(
$file,
$this->as3cf->filter_local->filter_post( $old_content )
);
}
}
}
}
/**
* Some widgets, specifically any standard WordPress widgets, make ajax requests back to the server
* before the edited section gets rendered in the Elementor editor. When they do, Elementor picks up
* properties directly from the saved post meta.
*
* @param array $instance
* @param array $new_instance
*
* @handles widget_update_callback
*
* @return mixed
*/
public function widget_update_callback( $instance, $new_instance ) {
return json_decode(
$this->as3cf->filter_local->filter_post( json_encode( $instance, JSON_UNESCAPED_SLASHES ) ),
true
);
}
/**
* Clears the Elementor cache
*
* @handles Multiple as3cf_*_completed and as3cf_*_cancelled actions
*/
public function clear_elementor_css_cache() {
if ( class_exists( '\Elementor\Plugin' ) ) {
Plugin::instance()->files_manager->clear_cache();
}
}
/**
* Rewrite URLs in Elementor frontend inline CSS before it's rendered/printed by WordPress
*
* @implements wp_print_styles
*/
public function wp_print_styles() {
$wp_styles = wp_styles();
if ( empty( $wp_styles->registered['elementor-frontend']->extra['after'] ) ) {
return;
}
foreach ( $wp_styles->registered['elementor-frontend']->extra['after'] as &$extra_css ) {
$filtered_css = $this->as3cf->filter_local->filter_post( $extra_css );
if ( ! empty( $filtered_css ) ) {
$extra_css = $filtered_css;
}
}
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Remove_Provider_Handler;
use Exception;
class Enable_Media_Replace extends Integration {
/**
* @var Media_Library
*/
private $media_library;
/**
* @var bool
*/
private $wait_for_generate_attachment_metadata = false;
/**
* @var string
*/
private $downloaded_original;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'EnableMediaReplace\EnableMediaReplacePlugin' ) || function_exists( 'enable_media_replace_init' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
$this->media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
}
/**
* @inheritDoc
*/
public function setup() {
// Make sure EMR allows OME to filter get_attached_file.
add_filter( 'emr_unfiltered_get_attached_file', '__return_false' );
// Download the files and return their path so EMR doesn't get tripped up.
add_filter( 'as3cf_get_attached_file', array( $this, 'download_file' ), 10, 4 );
// Although EMR uses wp_unique_filename, it discards that potentially new filename for plain replace, but does then use the following filter.
add_filter( 'emr_unique_filename', array( $this, 'ensure_unique_filename' ), 10, 3 );
// Remove objects before offload happens, but don't re-offload just yet.
add_filter( 'as3cf_update_attached_file', array( $this, 'remove_existing_provider_files_during_replace' ), 10, 2 );
if ( $this->is_replacing_media() ) {
$this->wait_for_generate_attachment_metadata = true;
// Let the media library integration know it should wait for all attachment metadata.
add_filter( 'as3cf_wait_for_generate_attachment_metadata', array( $this, 'wait_for_generate_attachment_metadata' ) );
// Wait for WordPress core to tell us it has finished generating thumbnails.
add_filter( 'wp_generate_attachment_metadata', array( $this, 'generate_attachment_metadata_done' ) );
// Add our potentially downloaded primary file to the list files to remove.
add_filter( 'as3cf_remove_local_files', array( $this, 'filter_remove_local_files' ) );
}
}
/**
* Are we waiting for the wp_generate_attachment_metadata filter to fire?
*
* @handles as3cf_wait_for_generate_attachment_metadata
*
* @param bool $wait
*
* @return bool
*/
public function wait_for_generate_attachment_metadata( $wait ) {
if ( $this->wait_for_generate_attachment_metadata ) {
return true;
}
return $wait;
}
/**
* Update internal state for waiting for attachment_metadata.
*
* @handles wp_generate_attachment_metadata
*
* @param array $metadata
*
* @return array
*/
public function generate_attachment_metadata_done( $metadata ) {
$this->wait_for_generate_attachment_metadata = false;
return $metadata;
}
/**
* If we've downloaded an existing primary file from the provider we add it to the
* files_to_remove array when the Remove_Local handler runs.
*
* @handles as3cf_remove_local_files
*
* @param array $files_to_remove
*
* @return array
*/
public function filter_remove_local_files( $files_to_remove ) {
if ( ! empty( $this->downloaded_original ) && file_exists( $this->downloaded_original ) && ! in_array( $this->downloaded_original, $files_to_remove ) ) {
$files_to_remove[] = $this->downloaded_original;
}
return $files_to_remove;
}
/**
* Allow the Enable Media Replace plugin to copy the provider file back to the local
* server when the file is missing on the server via get_attached_file().
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param Media_Library_Item $as3cf_item
*
* @return string
*/
public function download_file( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
$this->downloaded_original = $this->as3cf->plugin_compat->copy_image_to_server_on_action( 'media_replace_upload', false, $url, $file, $as3cf_item );
return $this->downloaded_original;
}
/**
* EMR deletes the original files before replace, then updates metadata etc.
* So we should remove associated offloaded files too, and let normal (re)offload happen afterwards.
*
* @param string $file
* @param int $attachment_id
*
* @return string
* @throws Exception
*/
public function remove_existing_provider_files_during_replace( $file, $attachment_id ) {
if ( ! $this->is_replacing_media() ) {
return $file;
}
if ( ! $this->as3cf->is_plugin_setup( true ) ) {
return $file;
}
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
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 ) );
// By deleting the item here, a new one will be created by when EMR generates the thumbnails and our ML integration
// picks it up. Ensuring that the object versioning string and object list are generated fresh.
$as3cf_item->delete();
}
return $file;
}
/**
* Are we doing a media replacement?
*
* @return bool
*/
public function is_replacing_media() {
$action = filter_input( INPUT_GET, 'action' );
if ( empty( $action ) ) {
return false;
}
return ( 'media_replace_upload' === sanitize_key( $action ) );
}
/**
* Ensure the generated filename for an image replaced with a new image is unique.
*
* @param string $filename File name that should be unique.
* @param string $path Absolute path to where the file will go.
* @param int $id Attachment ID.
*
* @return string
*/
public function ensure_unique_filename( $filename, $path, $id ) {
// Get extension.
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
$ext = $ext ? ".$ext" : '';
return $this->media_library->filter_unique_filename( $filename, $ext, $path, $id );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library;
use DeliciousBrains\WP_Offload_Media\Items\Download_Handler;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Items\Upload_Handler;
use Exception;
class Meta_Slider extends Integration {
/**
* @var Media_Library
*/
private $media_library;
/**
* Keep track of get_metadata recursion level
*
* @var int
*/
private $get_postmeta_recursion_level = 0;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'MetaSliderPlugin' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
$this->media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' );
}
/**
* @inheritDoc
*/
public function setup() {
add_filter( 'metaslider_attachment_url', array( $this, 'metaslider_attachment_url' ), 10, 2 );
add_filter( 'sanitize_post_meta_amazonS3_info', array( $this, 'layer_slide_sanitize_post_meta' ), 10, 3 );
add_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'layer_slide_abort_upload' ), 10, 4 );
add_filter( 'as3cf_remove_attachment_paths', array( $this, 'layer_slide_remove_attachment_paths' ), 10, 4 );
add_action( 'add_post_meta', array( $this, 'add_post_meta' ), 10, 3 );
add_action( 'update_post_meta', array( $this, 'update_post_meta' ), 10, 4 );
// Maybe download primary image.
add_filter( 'as3cf_get_attached_file', array( $this, 'download_for_resize' ), 10, 4 );
// Filter HTML in layer sliders when they are saved and fetched.
add_filter( 'sanitize_post_meta_ml-slider_html', array( $this, 'sanitize_layer_slider_html' ) );
add_filter( 'get_post_metadata', array( $this, 'filter_get_post_metadata' ), 10, 4 );
}
/**
* Use the Provider URL for a Meta Slider slide image.
*
* @handles metaslider_attachment_url
*
* @param string $url
* @param int $slide_id
*
* @return string
*/
public function metaslider_attachment_url( $url, $slide_id ) {
$provider_url = $this->media_library->wp_get_attachment_url( $url, $slide_id );
if ( ! is_wp_error( $provider_url ) && false !== $provider_url ) {
return $provider_url;
}
return $url;
}
/**
* Layer slide sanitize post meta.
*
* This fixes issues with 'Layer Slides', which uses `get_post_custom` to retrieve
* attachment meta, but does not unserialize the data. This results in the `amazonS3_info`
* key being double serialized when inserted into the database.
*
* @handles sanitize_post_meta_amazonS3_info
*
* @param mixed $meta_value
* @param string $meta_key
* @param string $object_type
*
* @return mixed
*
* Note: Legacy filter, kept for migration purposes.
*/
public function layer_slide_sanitize_post_meta( $meta_value, $meta_key, $object_type ) {
if ( ! $this->is_layer_slide() ) {
return $meta_value;
}
return AS3CF_Utils::maybe_unserialize( $meta_value );
}
/**
* Layer slide abort upload.
*
* 'Layer Slide' duplicates an attachment in the Media Library, but uses the same
* file as the original. This prevents us trying to upload a new version to the bucket.
*
* @handles as3cf_pre_update_attachment_metadata
*
* @param bool $pre
* @param mixed $data
* @param int $post_id
* @param Media_Library_Item|null $as3cf_item
*
* @return bool
*/
public function layer_slide_abort_upload( $pre, $data, $post_id, Media_Library_Item $as3cf_item = null ) {
if ( $this->is_layer_slide() && empty( $as3cf_item ) ) {
$original_id = filter_input( INPUT_POST, 'slide_id' );
if ( empty( $original_id ) ) {
return $pre;
}
$original_item = Media_Library_Item::get_by_source_id( $original_id );
if ( empty( $original_item ) ) {
return $pre;
}
$as3cf_item = Media_Library_Item::create_from_source_id( $post_id );
if ( empty( $as3cf_item ) ) {
return $pre;
}
$as3cf_item->set_path( $original_item->path() );
$as3cf_item->set_original_path( $original_item->original_path() );
$as3cf_item->set_extra_info( $original_item->extra_info() );
$as3cf_item->save();
return true;
}
return $pre;
}
/**
* Layer slide remove attachment paths.
*
* Because 'Layer Slide' duplicates an attachment in the Media Library, but uses the same
* file as the original we don't want to remove them from the bucket. Only the backup sizes
* should be removed.
*
* @handles as3cf_remove_attachment_paths
*
* @param array $paths
* @param int $post_id
* @param Media_Library_Item $item
* @param bool $remove_backup_sizes
*
* @return array
*/
public function layer_slide_remove_attachment_paths( $paths, $post_id, Media_Library_Item $item, $remove_backup_sizes ) {
$slider = get_post_meta( $post_id, 'ml-slider_type', true );
if ( 'html_overlay' !== $slider ) {
// Not a layer slide, return.
return $paths;
}
$meta = get_post_meta( $post_id, '_wp_attachment_metadata', true );
unset( $paths[ Media_Library_Item::primary_object_key() ] );
if ( isset( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $size => $details ) {
unset( $paths[ $size ] );
}
}
return $paths;
}
/**
* Is layer slide.
*
* @return bool
*/
private function is_layer_slide() {
if ( 'create_html_overlay_slide' === filter_input( INPUT_POST, 'action' ) ) {
return true;
}
return false;
}
/**
* Add post meta
*
* @handles add_post_meta
*
* @param int $object_id
* @param string $meta_key
* @param mixed $_meta_value
*
* @throws Exception
*/
public function add_post_meta( $object_id, $meta_key, $_meta_value ) {
$this->maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $_meta_value );
}
/**
* Update post meta
*
* @handles update_post_meta
*
* @param int $meta_id
* @param int $object_id
* @param string $meta_key
* @param mixed $_meta_value
*
* @throws Exception
*/
public function update_post_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
$this->maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $_meta_value );
}
/**
* Rewrites remote URLs to local when Meta Slider saves HTML layer slides.
*
* @handles sanitize_post_meta_ml-slider_html
*
* @param string $meta_value
*
* @return string
*/
public function sanitize_layer_slider_html( $meta_value ) {
return $this->as3cf->filter_provider->filter_post( $meta_value );
}
/**
* Rewrites remote URLs to local when Meta Slider gets HTML layer slides.
*
* @handles get_post_metadata
*
* @param mixed $check
* @param int $object_id
* @param string $meta_key
* @param mixed $meta_value
*
* @return string
*/
public function filter_get_post_metadata( $check, $object_id, $meta_key, $meta_value ) {
// Exit early if this is not our key to process.
if ( 'ml-slider_html' !== $meta_key ) {
return $check;
}
// We're calling get_metadata recursively and need to make sure
// we never nest deeper than one level.
if ( 0 === $this->get_postmeta_recursion_level ) {
$this->get_postmeta_recursion_level++;
$new_meta_value = get_metadata( 'post', $object_id, $meta_key, true );
$new_meta_value = $this->as3cf->filter_local->filter_post( $new_meta_value );
// Reset recursion.
$this->get_postmeta_recursion_level = 0;
return $new_meta_value;
}
return $check;
}
/**
* Allow meta slider to resize images that have been removed from local
*
* @handles as3cf_get_attached_file
*
* @param string $url
* @param string $file
* @param int $attachment_id
* @param Media_Library_Item $as3cf_item
*
* @return string
*/
public function download_for_resize( $url, $file, $attachment_id, Media_Library_Item $as3cf_item ) {
$action = filter_input( INPUT_POST, 'action' );
if ( ! in_array( $action, array( 'resize_image_slide', 'create_html_overlay_slide' ) ) ) {
return $url;
}
$download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() );
$result = $download_handler->handle( $as3cf_item, array( 'full_source_paths' => array( $file ) ) );
if ( empty( $result ) || is_wp_error( $result ) ) {
return $url;
}
return $file;
}
/**
* Maybe upload attachment backup sizes
*
* @param int $object_id
* @param string $meta_key
* @param mixed $data
*
* @throws Exception
*/
private function maybe_upload_attachment_backup_sizes( $object_id, $meta_key, $data ) {
if ( '_wp_attachment_backup_sizes' !== $meta_key ) {
return;
}
if ( 'resize_image_slide' !== filter_input( INPUT_POST, 'action' ) && ! $this->is_layer_slide() ) {
return;
}
if ( ! $this->as3cf->is_plugin_setup( true ) ) {
return;
}
$item = Media_Library_Item::get_by_source_id( $object_id );
if ( ! $item && ! $this->as3cf->get_setting( 'copy-to-s3' ) ) {
// Abort if not already offloaded to provider and the copy setting is off.
return;
}
$this->upload_attachment_backup_sizes( $object_id, $item, $data );
}
/**
* Upload attachment backup sizes
*
* @param int $object_id
* @param Media_Library_Item $as3cf_item
* @param mixed $data
*
* @throws Exception
*/
private function upload_attachment_backup_sizes( $object_id, Media_Library_Item $as3cf_item, $data ) {
foreach ( $data as $key => $file ) {
if ( ! isset( $file['path'] ) ) {
continue;
}
$objects = $as3cf_item->objects();
if ( ! empty( $objects[ $key ] ) ) {
continue;
}
$options = array(
'offloaded_files' => $as3cf_item->offloaded_files(),
);
$objects[ $key ] = array(
'source_file' => wp_basename( $file['path'] ),
'is_private' => false,
);
$as3cf_item->set_objects( $objects );
$upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() );
$upload_handler->handle( $as3cf_item, $options );
}
}
}

View File

@@ -0,0 +1,532 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use Amazon_S3_And_CloudFront_Pro;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Pro\Items\Update_Acl_Handler;
use Exception;
use WC_Product;
use WC_Product_Download;
class Woocommerce extends Integration {
/**
* Keep track of URLs that we already transformed to remote URLs
* when product object was re-hydrated
*
* @var array
*/
private $re_hydrated_urls = array();
/**
* @var Amazon_S3_And_CloudFront_Pro
*/
protected $as3cf;
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'WooCommerce' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
// Nothing to do.
}
/**
* @inheritDoc
*/
public function setup() {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
add_action( 'woocommerce_process_product_file_download_paths', array( $this, 'make_files_private_on_provider' ), 10, 3 );
add_filter( 'woocommerce_file_download_path', array( $this, 'woocommerce_file_download_path' ), 20, 1 );
add_action( 'woocommerce_admin_process_product_object', array( $this, 'woocommerce_admin_process_product_object' ), 10, 1 );
add_action( 'woocommerce_admin_process_variation_object', array( $this, 'woocommerce_admin_process_product_object' ), 10, 1 );
add_action( 'woocommerce_download_file_as3cf', array( $this, 'download_file' ), 10, 2 );
add_filter( 'woocommerce_file_download_method', array( $this, 'add_download_method' ) );
}
/**
* Enqueue scripts
*
* @return void
*/
public function admin_scripts() {
$screen = get_current_screen();
if ( in_array( $screen->id, array( 'product', 'edit-product' ) ) ) {
if ( ! $this->as3cf->is_pro_plugin_setup( true ) ) {
// Don't allow new shortcodes if Pro not set up
return;
}
wp_enqueue_media();
$this->as3cf->enqueue_script( 'as3cf-woo-script', 'assets/js/pro/integrations/woocommerce', array(
'jquery',
'wp-util',
) );
wp_localize_script( 'as3cf-woo-script', 'as3cf_woo', array(
'strings' => array(
'media_modal_title' => __( 'Select Downloadable File', 'as3cf-woocommerce' ),
'media_modal_button' => __( 'Insert File', 'as3cf-woocommerce' ),
'input_placeholder' => __( 'Retrieving...', 'as3cf-woocommerce' ),
),
'nonces' => array(
'is_amazon_provider_attachment' => wp_create_nonce( 'as3cf_woo_is_amazon_provider_attachment' ),
),
) );
}
}
/**
* Make file private on provider.
*
* @param int $post_id
* @param int $variation_id
* @param array $files
*
* @return array
*/
public function make_files_private_on_provider( $post_id, $variation_id, $files ) {
$new_attachments = array();
$post_id = $variation_id > 0 ? $variation_id : $post_id;
/** @var Update_Acl_Handler $acl_handler */
$acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() );
foreach ( $files as $file ) {
$url = $this->downloadable_file_url( $file );
$item_source = ! empty( $url ) ? $this->as3cf->filter_local->get_item_source_from_url( $url ) : false;
if ( false !== $item_source && ! Media_Library_Item::is_empty_item_source( $item_source ) ) {
$attachment_id = $item_source['id'];
} else {
// Attachment id could not be determined, ignore
continue;
}
$size = $this->as3cf->filter_local->get_size_string_from_url( $item_source, $url );
$new_attachments[] = $attachment_id . '-' . $size;
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! $as3cf_item ) {
// Not offloaded, ignore.
continue;
}
if ( $as3cf_item->is_private( $size ) ) {
// Item is already private, carry on
continue;
}
// Only set new files as private if the Pro plugin is setup
if ( $this->as3cf->is_pro_plugin_setup( true ) ) {
$options = array(
'object_keys' => array( $size ),
'set_private' => true,
);
$result = $acl_handler->handle( $as3cf_item, $options );
if ( true === $result ) {
$this->as3cf->make_acl_admin_notice( $as3cf_item, $size );
}
}
}
$this->maybe_make_removed_files_public( $post_id, $new_attachments );
return $files;
}
/**
* Maybe rewrite WooCommerce product file value to provider URL.
*
* @handles woocommerce_file_download_path
*
* @param string $file
*
* @return string
*/
public function woocommerce_file_download_path( $file ) {
$size = null;
$remote_url = false;
$attachment_id = 0;
// Is it a local URL ?
$item_source = $this->as3cf->filter_local->get_item_source_from_url( $file );
if ( false !== $item_source && ! Media_Library_Item::is_empty_item_source( $item_source ) ) {
$attachment_id = $item_source['id'];
}
if ( $attachment_id > 0 ) {
$size = $this->as3cf->filter_local->get_size_string_from_url( $item_source, $file );
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! empty( $as3cf_item ) ) {
$remote_url = $as3cf_item->get_provider_url( $size );
}
}
// Is it our shortcode ?
$atts = $this->get_shortcode_atts( $file );
if ( isset( $atts['id'] ) ) {
$attachment_id = (int) $this->get_attachment_id_from_shortcode( $file );
if ( $attachment_id > 0 ) {
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! empty( $as3cf_item ) ) {
$remote_url = $as3cf_item->get_provider_url();
}
}
}
if ( is_string( $remote_url ) && ! empty( $remote_url ) ) {
$this->re_hydrated_urls[ $remote_url ] = array(
'id' => $attachment_id,
'size' => $size,
);
return $remote_url;
}
return $file;
}
/**
* Maybe rewrite WooCommerce product file URLs to local URLs.
*
* @handles woocommerce_admin_process_product_object
* @handles woocommerce_admin_process_variation_object
*
* @param WC_Product $product
*/
public function woocommerce_admin_process_product_object( $product ) {
$downloads = $product->get_downloads();
foreach ( $downloads as $download ) {
$url = $this->downloadable_file_url( $download );
// Is this a shortcode ?
$attachment_id = (int) $this->get_attachment_id_from_shortcode( $url );
// If not, is it a remote URL?
if ( ! $attachment_id ) {
$item_source = $this->as3cf->filter_provider->get_item_source_from_url( $url );
if ( false !== $item_source && ! Media_Library_Item::is_empty_item_source( $item_source ) ) {
$attachment_id = $item_source['id'];
}
}
if ( $attachment_id > 0 ) {
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( false !== $as3cf_item ) {
$size = $this->as3cf->filter_local->get_size_string_from_url( $as3cf_item->get_item_source_array(), $url );
$url = $as3cf_item->get_local_url( $size );
$download->set_file( $url );
}
}
}
}
/**
* Get attachment id from shortcode.
*
* @param string $shortcode
*
* @return int|bool
*/
public function get_attachment_id_from_shortcode( $shortcode ) {
$atts = $this->get_shortcode_atts( $shortcode );
if ( isset( $atts['id'] ) ) {
return intval( $atts['id'] );
}
if ( ! isset( $atts['bucket'] ) || ! isset( $atts['object'] ) ) {
return false;
}
return Media_Library_Item::get_source_id_by_bucket_and_path( $atts['bucket'], $atts['object'] );
}
/**
* Get shortcode atts.
*
* @param string $shortcode
*
* @return array
*/
public function get_shortcode_atts( $shortcode ) {
$shortcode = trim( stripcslashes( $shortcode ) );
$shortcode = ltrim( $shortcode, '[' );
$shortcode = rtrim( $shortcode, ']' );
$shortcode = shortcode_parse_atts( $shortcode );
return $shortcode;
}
/**
* Remove private ACL from provider if no longer used by WooCommerce.
*
* @param int $post_id
* @param array $new_attachments List of attachments. Attachment id AND size on the format "$id-$size"
*
* @return void
*/
protected function maybe_make_removed_files_public( $post_id, $new_attachments ) {
$old_files = get_post_meta( $post_id, '_downloadable_files', true );
$old_attachments = array();
/** @var Update_Acl_Handler $acl_handler */
$acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() );
if ( is_array( $old_files ) ) {
foreach ( $old_files as $old_file ) {
$url = $this->downloadable_file_url( $old_file );
$item_source = ! empty( $url ) ? $this->as3cf->filter_local->get_item_source_from_url( $url ) : false;
if ( false !== $item_source && ! Media_Library_Item::is_empty_item_source( $item_source ) ) {
$size = $this->as3cf->filter_local->get_size_string_from_url( $item_source, $url );
$old_attachments[] = $item_source['id'] . '-' . $size;
}
}
}
$removed_attachments = array_diff( $old_attachments, $new_attachments );
if ( empty( $removed_attachments ) ) {
return;
}
global $wpdb;
foreach ( $removed_attachments as $attachment ) {
$parts = explode( '-', $attachment );
$attachment_id = (int) $parts[0];
$size = empty( $parts[1] ) ? null : $parts[1];
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! $as3cf_item ) {
// Not offloaded, ignore.
continue;
}
if ( ! $as3cf_item->is_private( $size ) ) {
// Item is already public, carry on
continue;
}
$local_url = AS3CF_Utils::reduce_url( strval( $as3cf_item->get_local_url( $size ) ) );
$file = AS3CF_Utils::is_full_size( $size ) ? null : wp_basename( $as3cf_item->path( $size ) );
$bucket = preg_quote( $as3cf_item->bucket(), '@' );
$key = preg_quote( $as3cf_item->key( $file ), '@' );
$url = preg_quote( $local_url, '@' );
// Check the attachment isn't used by other downloads
$sql = $wpdb->prepare( "
SELECT meta_value
FROM $wpdb->postmeta
WHERE post_id != %d
AND meta_key = %s
AND (meta_value LIKE %s OR meta_value like %s)
", $post_id, '_downloadable_files', '%amazon_s3%', '%' . $local_url . '%' );
$results = $wpdb->get_results( $sql, ARRAY_A );
foreach ( $results as $result ) {
// WP Offload Media
if ( preg_match( '@\[amazon_s3\sid=[\'\"]*' . $attachment_id . '[\'\"]*\]@', $result['meta_value'] ) ) {
continue 2;
}
// Official WooCommerce S3 addon
if ( preg_match( '@\[amazon_s3\sobject=[\'\"]*' . $key . '[\'\"]*\sbucket=[\'\"]*' . $bucket . '[\'\"]*\]@', $result['meta_value'] ) ) {
continue 2;
}
if ( preg_match( '@\[amazon_s3\sbucket=[\'\"]*' . $bucket . '[\'\"]*\sobject=[\'\"]*' . $key . '[\'\"]*\]@', $result['meta_value'] ) ) {
continue 2;
}
if ( preg_match( '@' . $url . '@', $result['meta_value'] ) ) {
continue 2;
}
}
// Set ACL to public
$options = array(
'object_keys' => array( $size ),
'set_private' => false,
);
$result = $acl_handler->handle( $as3cf_item, $options );
if ( true === $result ) {
$this->as3cf->make_acl_admin_notice( $as3cf_item, $size );
}
}
}
/**
* Add download method to WooCommerce.
*
* @return string
*/
public function add_download_method() {
return 'as3cf';
}
/**
* Use S3 secure link to download file.
*
* @param string $file_path
* @param int $filename
*
* @return void
*/
public function download_file( $file_path, $filename ) {
$size = null;
$attachment_id = 0;
/*
* Is this a remote URL that we already handled when the product object
* was re-hydrated?
*/
if ( isset( $this->re_hydrated_urls[ $file_path ] ) ) {
$attachment_id = $this->re_hydrated_urls[ $file_path ]['id'];
$size = $this->re_hydrated_urls[ $file_path ]['size'];
}
/*
* Is this a shortcode that resolves to an attachment?
*/
if ( ! $attachment_id ) {
$attachment_id = (int) $this->get_attachment_id_from_shortcode( $file_path );
}
/*
* If no attachment was found via shortcode, it's possible that
* $file_path is a URL to the local version of an offloaded item
*/
if ( ! $attachment_id ) {
$item_source = $this->as3cf->filter_local->get_item_source_from_url( $file_path );
if ( false !== $item_source && ! Media_Library_Item::is_empty_item_source( $item_source ) ) {
$attachment_id = $item_source['id'];
$size = $this->as3cf->filter_local->get_size_string_from_url( $item_source, $file_path );
}
}
$expires = apply_filters( 'as3cf_woocommerce_download_expires', 5 );
$file_data = array(
'name' => $filename,
'file' => $file_path,
);
if ( ! $attachment_id || ! Media_Library_Item::get_by_source_id( $attachment_id ) ) {
/*
This addon is meant to be a drop-in replacement for the
WooCommerce Amazon S3 Storage extension. The latter doesn't encourage people
to add the file to the Media Library, so even though we can't get an
attachment ID for the shortcode, we should still serve the download
if the shortcode contains the `bucket` and `object` attributes.
*/
$atts = $this->get_shortcode_atts( $file_path );
if ( isset( $atts['bucket'] ) && isset( $atts['object'] ) ) {
$bucket_setting = $this->as3cf->get_setting( 'bucket' );
if ( $bucket_setting === $atts['bucket'] ) {
$region = $this->as3cf->get_setting( 'region' );
} else {
$region = $this->as3cf->get_bucket_region( $atts['bucket'] );
}
if ( is_wp_error( $region ) ) {
return;
}
try {
$expires = time() + $expires;
$headers = apply_filters( 'as3cf_woocommerce_download_headers', array( 'ResponseContentDisposition' => 'attachment' ), $file_data );
$secure_url = $this->as3cf->get_provider_client( $region, true )->get_object_url( $atts['bucket'], $atts['object'], $expires, $headers );
} catch ( Exception $e ) {
return;
}
add_filter( 'wp_die_handler', array( $this, 'set_wp_die_handler' ) );
header( 'Location: ' . $secure_url );
wp_die();
}
// Handle shortcode inputs where the file has been removed from S3
// Parse the url, shortcodes do not return a host
$url = parse_url( $file_path );
if ( ! isset( $url['host'] ) && ! empty( $attachment_id ) ) {
$file_path = wp_get_attachment_url( $attachment_id );
$filename = wp_basename( $file_path );
}
// File not on S3, trigger WooCommerce saved download method
$method = get_option( 'woocommerce_file_download_method', 'force' );
do_action( 'woocommerce_download_file_' . $method, $file_path, $filename );
} else {
$file_data['attachment_id'] = $attachment_id;
$headers = apply_filters( 'as3cf_woocommerce_download_headers', array( 'ResponseContentDisposition' => 'attachment' ), $file_data );
$as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( ! empty( $as3cf_item ) ) {
$secure_url = $as3cf_item->get_provider_url( $size, $expires, $headers );
add_filter( 'wp_die_handler', array( $this, 'set_wp_die_handler' ) );
header( 'Location: ' . $secure_url );
wp_die();
}
}
}
/**
* Filter handler for set_wp_die_handler.
*
* @param array $handler
*
* @return array
*/
public function set_wp_die_handler( $handler ) {
return array( $this, 'woocommerce_die_handler' );
}
/**
* Replacement for the default wp_die() handler
*/
public function woocommerce_die_handler() {
exit;
}
/**
* Get the downloadable file URL from WooCommerce object
*
* @param WC_Product_Download|array $file
*
* @return string
*/
private function downloadable_file_url( $file ) {
if ( $file instanceof WC_Product_Download ) {
return $file->get_file();
} elseif ( is_array( $file ) && isset( $file['file'] ) ) {
return $file['file'];
}
return '';
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Pro\Integrations;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use DeliciousBrains\WP_Offload_Media\Integrations\Integration;
class Wpml extends Integration {
/**
* Is installed?
*
* @return bool
*/
public static function is_installed(): bool {
if ( class_exists( 'SitePress' ) ) {
return true;
}
return false;
}
/**
* Init integration.
*/
public function init() {
// Nothing to do.
}
/**
* @inheritDoc
*/
public function setup() {
add_action( 'wpml_media_create_duplicate_attachment', array( $this, 'duplicate_offloaded_item' ), 10, 2 );
}
/**
* Duplicate original item's offload data for new duplicate Media Library item.
*
* WPML duplicates postmeta in reverse order which unfortunately means we can't catch the new attachment and offload it.
* But, WPML does fire an action after each item is duplicated, so we can just duplicate our data too.
*
* @param int $attachment_id
* @param int $new_attachment_id
*/
public function duplicate_offloaded_item( $attachment_id, $new_attachment_id ) {
$old_item = Media_Library_Item::get_by_source_id( $attachment_id );
if ( $old_item ) {
$as3cf_item = Media_Library_Item::get_by_source_id( $new_attachment_id );
if ( ! $as3cf_item ) {
$as3cf_item = new Media_Library_Item(
$old_item->provider(),
$old_item->region(),
$old_item->bucket(),
$old_item->path(),
$old_item->is_private(),
$new_attachment_id,
$old_item->source_path(),
wp_basename( $old_item->original_source_path() ),
$old_item->extra_info()
);
$as3cf_item->save();
$as3cf_item->duplicate_filesize_total( $attachment_id );
}
}
}
}