Files
WPS3Media/classes/pro/integrations/elementor.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

243 lines
7.6 KiB
PHP

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