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

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

234 lines
6.3 KiB
PHP

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