Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image Optimization: Add Rate Limiting and Bans #75

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions includes/Images/ImageLimitBanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace NewfoldLabs\WP\Module\Performance\Images;

/**
* Displays admin notices for rate limits and bans in the WP Admin area.
*/
class ImageLimitBanner {
/**
* The active brand plugin.
*
* @var string
*/
private $brand;

/**
* Initializes the Image Limit or Ban Banner.
*
* @param Container $container Dependency injection container.
*/
public function __construct( $container ) {
$this->brand = $container->plugin()->id;
add_action( 'admin_notices', array( $this, 'display_admin_banner' ) );
}

/**
* Displays the admin banner for rate limits or bans.
*/
public function display_admin_banner() {
// Check for rate limiting.
$rate_limit_time = get_transient( ImageService::$rate_limit_transient_key );
if ( $rate_limit_time ) {
$this->display_rate_limit_banner( $rate_limit_time );
return;
}

// Check for permanent ban.
$is_banned = get_option( ImageService::$ban_site_option_key, false );
if ( $is_banned ) {
$this->display_ban_banner();
}
}

/**
* Displays the rate limit banner.
*
* @param int $rate_limit_time Timestamp when the rate limit will expire.
*/
private function display_rate_limit_banner( $rate_limit_time ) {
$retry_after = human_time_diff( time(), $rate_limit_time );

echo '<div class="notice notice-warning is-dismissible">';
echo '<p>';
printf(
/* translators: %s: Time remaining */
esc_html__( 'Your site has been rate limited for image optimization. Please try again after %s.', 'wp-module-performance' ),
esc_html( $retry_after )
);
echo '</p>';
echo '</div>';
}

/**
* Displays the permanent ban banner.
*/
private function display_ban_banner() {
$support_link = admin_url( "admin.php?page={$this->brand}#/help" );

echo '<div class="notice notice-error">';
echo '<p>';
printf(
wp_kses(
/* translators: %s: Support link */
__( 'Your site has been permanently banned from image optimization due to excessive usage. Please <a href="%s">contact support</a> for assistance.', 'wp-module-performance' ),
array(
'a' => array(
'href' => array(),
),
)
),
esc_url( $support_link )
);
echo '</p>';
echo '</div>';
}
}
12 changes: 12 additions & 0 deletions includes/Images/ImageManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ private function initialize_services( Container $container ) {
$this->maybe_initialize_rest_api();
$this->maybe_initialize_marker();
$this->maybe_initialize_image_rewrite_handler( $container );
$this->maybe_initialize_image_limit_banner( $container );
}

/**
Expand Down Expand Up @@ -102,4 +103,15 @@ private function maybe_initialize_image_rewrite_handler( Container $container )
new ImageRewriteHandler();
}
}

/**
* Conditionally initializes the Image Limit Banner in the WordPress admin area.
*
* @param Container $container Dependency injection container.
*/
private function maybe_initialize_image_limit_banner( $container ) {
if ( ImageSettings::is_optimization_enabled() && Permissions::is_authorized_admin() ) {
new ImageLimitBanner( $container );
}
}
}
77 changes: 73 additions & 4 deletions includes/Images/ImageService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ class ImageService {
*/
private const WORKER_URL = 'https://hiive.cloud/workers/image-optimization';

/**
* Rate limit transient key.
*
* @var string
*/
public static $rate_limit_transient_key = 'nfd_image_optimization_rate_limit';

/**
* Ban site option key.
*
* @var string
*/
public static $ban_site_option_key = 'nfd_image_optimization_ban';

/**
* Optimizes an uploaded image by sending it to the Cloudflare Worker and saving the result as WebP.
*
Expand All @@ -28,12 +42,44 @@ public function optimize_image( $image_url, $original_file_path ) {
);
}

$site_url = get_site_url();
if ( ! $site_url ) {
return new \WP_Error(
'nfd_performance_error',
__( 'Error retrieving site URL.', 'wp-module-performance' )
);
}

// Check if the site is permanently banned
if ( get_option( 'nfd_image_optimization_ban', false ) ) {
return new \WP_Error(
'nfd_performance_error',
__( 'Image optimization access has been permanently revoked for this site.', 'wp-module-performance' )
);
}

// Check for rate limiting
$rate_limit_transient = get_transient( self::$rate_limit_transient_key );
if ( $rate_limit_transient ) {
return new \WP_Error(
'nfd_performance_error',
sprintf(
/* translators: %s: Retry time in seconds */
__( 'Rate limit exceeded. Please retry after %s.', 'wp-module-performance' ),
human_time_diff( time(), $rate_limit_transient )
)
);
}

// Make a POST request to the Cloudflare Worker
$response = wp_remote_post(
self::WORKER_URL . '/?image=' . rawurlencode( $image_url ),
array(
'method' => 'POST',
'timeout' => 30,
'headers' => array(
'X-Site-Url' => $site_url,
),
)
);

Expand All @@ -49,19 +95,35 @@ public function optimize_image( $image_url, $original_file_path ) {
);
}

// Check for HTTP 400-series errors
// Check for HTTP errors
$response_code = wp_remote_retrieve_response_code( $response );
if ( 400 <= $response_code && 499 >= $response_code ) {
if ( 403 === $response_code ) {
// If worker indicates a permanent ban, ban the site
$this->ban_site();
return new \WP_Error(
'nfd_performance_error',
__( 'Image optimization access has been permanently revoked for this site.', 'wp-module-performance' )
);
} elseif ( 429 === $response_code ) {
// Set a transient for the retry period
$retry_after = wp_remote_retrieve_header( $response, 'Retry-After' );
$retry_seconds = $retry_after ? intval( $retry_after ) : 60;
set_transient( self::$rate_limit_transient_key, time() + $retry_seconds, $retry_seconds );
return new \WP_Error(
'nfd_performance_error',
__( 'Rate limit exceeded. Please try again later.', 'wp-module-performance' )
);
} elseif ( 400 <= $response_code && 499 >= $response_code ) {
$error_message = $this->get_response_message( $response );
return new \WP_Error(
'nfd_performance_error',
sprintf(
/* translators: %s: Error message */
/* translators: %s: Error Message */
__( 'Client error from Cloudflare Worker: %s', 'wp-module-performance' ),
$error_message
)
);
} elseif ( 500 <= $response_code ) { // Yoda condition
} elseif ( 500 <= $response_code ) {
return new \WP_Error(
'nfd_performance_error',
__( 'Server error from Cloudflare Worker. Please try again later.', 'wp-module-performance' )
Expand Down Expand Up @@ -93,6 +155,13 @@ public function optimize_image( $image_url, $original_file_path ) {
return $webp_file_path;
}

/**
* Permanently ban the site from accessing image optimization.
*/
private function ban_site() {
update_option( self::$ban_site_option_key, true );
}

/**
* Generates a WebP file path based on the original file path.
*
Expand Down
Loading