File: /var/www/html/wp-content/plugins/breeze/inc/class-breeze-cloudflare-helper.php
<?php
if ( ! defined( 'ABSPATH' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit;
}
/**
* This class handles Cloudways - CloudFlare microservice action.
*
* @since 2.0.15
* @final
*/
final class Breeze_CloudFlare_Helper {
private $cw_platform = '';
private static $processed_home_urls = array();
function __construct() {
add_action( 'switch_theme', array( &$this, 'clear_cf_on_changing_theme' ), 11, 3 );
add_action( 'breeze_scheduled_purge', array( &$this, 'execute_purge' ), 10, 2 );
}
/**
* Define Microservice url.
*
* @return false|string
* @since 2.0.15
* @private
*/
private function get_microservice_url() {
if ( false === self::is_cloudflare_enabled() ) {
return false;
}
$fpc_microservice_url = ''; // default
/**
* Contains the dynamic microservice URL.
*/
if ( true === self::is_fp_server() ) {
$this->cw_platform = 'fp';
$fpc_microservice_url = getenv( 'FPC_ENV' ); // TODO Add the hardcoded link for Flexible (stating|production).
if ( true === self::is_log_enabled() ) {
$server_type_text = '';
if ( true === self::is_staging_server() ) {
$server_type_text = 'Staging';
}
if ( true === self::is_production_server() ) {
$server_type_text = 'Production';
}
error_log( 'Cloudways FP (Flexible) is ON: ' . $server_type_text );
}
} elseif ( ! empty( getenv( 'FPC_ENV' ) ) ) {
$this->cw_platform = 'fmp';
$fpc_microservice_url = getenv( 'FPC_ENV' );// FMP
if ( true === self::is_log_enabled() ) {
error_log( 'Cloudways FMP (Autoscale) is ON ' );
}
}
if ( true === self::is_log_enabled() ) {
error_log( 'Microservice URL: ' . var_export( $fpc_microservice_url, true ) );
}
if ( empty( $fpc_microservice_url ) ) {
return false;
}
return trailingslashit( $fpc_microservice_url );
}
/**
* Purge Cloudflare cache on theme switch.
*
* @param string $new_name Name of the new theme.
* @param string $new_theme WP_Theme instance of the new theme.
* @param string $old_theme WP_Theme instance of the old theme.
*
* @return void
* @since 2.0.15
* @public
*/
public function clear_cf_on_changing_theme( string $new_name, string $new_theme, string $old_theme ) {
$list_of_urls[] = get_home_url();
Breeze_CloudFlare_Helper::reset_all_cache( $list_of_urls );
}
/**
* Clear the cache for the given url list.
* Needs at least one element.
*
* @param array $specific_urls Array with the list of URLs.
* @param string $purge_type Purge type: 'default', 'cron'
*
* @return bool|string|null
* @since 2.0.15
* @access public
* @static
*/
public static function purge_cloudflare_cache_urls( array $specific_urls = array(), string $purge_type = 'default' ) {
// If we do not have everything that we need, stop the process.
if ( true === self::is_log_enabled() ) {
error_log( '######### PURGE CLOUDFLARE ###: ' . var_export( 'Single URL START', true ) );
}
if ( false === self::is_cloudflare_enabled() ) {
return false;
}
// Remove any non URL items.
$specific_urls = ( new Breeze_CloudFlare_Helper() )->remove_not_url_elements( $specific_urls );
if ( true === self::is_log_enabled() ) {
error_log( 'single url : ' . var_export( $specific_urls, true ) );
}
// Call cache reset.
return ( new Breeze_CloudFlare_Helper() )->request_cache_reset( $specific_urls, 'purge-fpc-url', $purge_type );
}
/**
* Purge all cache in CloudFlare.
* In multisite clears for all sub-sites.
*
* @param array $home_url Used by WP-CLI
*
* @return bool|string|null
* @since 2.0.15
* @access public
* @static
*/
public static function reset_all_cache( array $home_url = array() ) {
// If we do not have everything that we need, stop the process.
if ( false === self::is_cloudflare_enabled() ) {
return false;
}
/**
* Execute code if this function is not called by WP-CLI.
*/
if ( empty( $home_url ) ) {
// For multisite network, clear cache for all sub-sites.
if ( ( is_multisite() && is_network_admin() ) ) {
$blogs = get_sites();
foreach ( $blogs as $blog_data ) {
$url = get_home_url( $blog_data->blog_id );
$home_url[] = trailingslashit( $url );
}
} else {
$home_url[] = trailingslashit( home_url() );
}
}
$home_url_key = hash( 'sha256', serialize( $home_url ) );
if ( isset( self::$processed_home_urls[ $home_url_key ] ) ) {
return true;
}
$purge_request_endpoint = 'purge-fpc-domain';
if ( is_multisite() ) {
if ( is_subdomain_install() ) {
$home_url = ( new Breeze_CloudFlare_Helper() )->clear_domain_purge_urls( $home_url );
if ( true === self::is_log_enabled() ) {
error_log( '######### CF SubDomains: ' . var_export( $home_url, true ) );
}
} else {
$purge_request_endpoint = 'purge-fpc-sub-dir';
if ( ! empty( $home_url ) ) {
foreach ( $home_url as &$url ) {
$url = untrailingslashit( $url );
}
if ( true === self::is_log_enabled() ) {
error_log( '######### CF SubDirectory: ' . var_export( $home_url, true ) );
}
}
}
} else {
$home_url = ( new Breeze_CloudFlare_Helper() )->clear_domain_purge_urls( $home_url );
if ( true === self::is_log_enabled() ) {
error_log( '######### CF Domain: ' . var_export( $home_url, true ) );
}
}
// Adjust endpoint based on WPML language URL format settings.
$breeze_helper = new Breeze_CloudFlare_Helper();
$purge_request_endpoint = $breeze_helper->get_wpml_adjusted_endpoint( $purge_request_endpoint );
// If endpoint was changed to sub-dir, we need to process URLs accordingly.
if ( 'purge-fpc-sub-dir' === $purge_request_endpoint && ! is_multisite() ) {
if ( ! empty( $home_url ) ) {
foreach ( $home_url as &$url ) {
$url = untrailingslashit( $url );
}
if ( true === self::is_log_enabled() ) {
error_log( '######### CF WPML SubDirectory: ' . var_export( $home_url, true ) );
}
}
}
self::$processed_home_urls[ $home_url_key ] = true;
return $breeze_helper->request_cache_reset( $home_url, $purge_request_endpoint );
}
/**
* Adjust the purge endpoint based on WPML language URL format settings.
*
* This method checks if WPML is active and configured to always show language
* directories in URLs. If both conditions are met, it changes the endpoint
* from 'purge-fpc-domain' to 'purge-fpc-sub-dir'.
*
* @param string $current_endpoint The current purge request endpoint.
*
* @return string The adjusted endpoint based on WPML settings.
* @since 2.0.20
* @access private
*/
private function get_wpml_adjusted_endpoint( string $current_endpoint ): string {
// Only adjust if current endpoint is 'purge-fpc-domain'.
if ( 'purge-fpc-domain' !== $current_endpoint ) {
return $current_endpoint;
}
// Check if WPML is active.
if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
if ( true === self::is_log_enabled() ) {
error_log( 'WPML is not active, keeping endpoint: ' . $current_endpoint );
}
return $current_endpoint;
}
// Get WPML settings.
$wpml_settings = get_option( 'icl_sitepress_settings' );
if ( empty( $wpml_settings ) ) {
if ( true === self::is_log_enabled() ) {
error_log( 'WPML settings not found, keeping endpoint: ' . $current_endpoint );
}
return $current_endpoint;
}
// Check language negotiation type (must be 1 for directories).
$language_negotiation_type = isset( $wpml_settings['language_negotiation_type'] ) ? absint( $wpml_settings['language_negotiation_type'] ) : 0;
// Check if directory for default language is enabled.
$directory_for_default_language = isset( $wpml_settings['urls']['directory_for_default_language'] ) ? (bool) $wpml_settings['urls']['directory_for_default_language'] : false;
if ( true === self::is_log_enabled() ) {
error_log( 'WPML language_negotiation_type: ' . var_export( $language_negotiation_type, true ) );
error_log( 'WPML directory_for_default_language: ' . var_export( $directory_for_default_language, true ) );
}
// If both conditions are met, change endpoint to subdirectory purge.
if ( 1 === $language_negotiation_type && true === $directory_for_default_language ) {
$adjusted_endpoint = 'purge-fpc-sub-dir';
if ( true === self::is_log_enabled() ) {
error_log( 'WPML language directories detected, changing endpoint from ' . $current_endpoint . ' to ' . $adjusted_endpoint );
}
return $adjusted_endpoint;
}
if ( true === self::is_log_enabled() ) {
error_log( 'WPML conditions not met, keeping endpoint: ' . $current_endpoint );
}
return $current_endpoint;
}
/** @return bool True if any purge URL contains a WPML directory language segment (e.g. /sk/). */
private function purge_urls_have_wpml_language_directory( array $purge_url_list ): bool {
$langs = defined( 'ICL_SITEPRESS_VERSION' ) ? apply_filters( 'wpml_active_languages', null, array( 'skip_missing' => 0 ) ) : null;
return is_array( $langs ) && ! empty( $langs ) && (bool) preg_grep( '#/(?:' . implode( '|', array_map( function ( $c ) { return preg_quote( strtolower( $c ), '#' ); }, array_keys( $langs ) ) ) . ')(?:/|$)#i', $purge_url_list );
}
/**
* Clear the list of URLs of HTTP schema and remove the slash at the end.
* This is needed for domain CF purge.
*
* @param array $url_list List of URLs.
*
* @return array
*/
private function clear_domain_purge_urls( array $url_list = array() ): array {
if ( empty( $url_list ) ) {
return $url_list;
}
foreach ( $url_list as &$url ) {
$url = trim( $url );
$url = ltrim( $url, 'https:' );
$url = ltrim( $url, '//' );
$url = untrailingslashit( $url );
}
return $url_list;
}
/**
* Remove all array elements which are not a valid URL.
*
* @param array $url_list Given url list.
*
* @return array
*
* @access private
* @since 2.0.15
*/
private function remove_not_url_elements( array $url_list = array() ): array {
// Remove any white spaces from URL list.
$url_list = array_map( 'trim', $url_list );
// Making sure there are no duplicates.
$url_list = array_unique( $url_list );
return array_filter(
$url_list,
function ( $value, $index ) {
return false !== filter_var( $value, FILTER_VALIDATE_URL );
},
ARRAY_FILTER_USE_BOTH
);
}
/**
* Will return true if defined constants are found.
*
* @return bool
*
* @since 2.0.15
* @access public
* @static
*/
public static function is_cloudflare_enabled(): bool {
$return_value = true;
if (
! defined( 'CDN_SITE_ID' ) ||
! defined( 'CDN_SITE_TOKEN' )
) {
if ( true === self::is_log_enabled() ) {
error_log( 'Error: CDN_SITE_ID or CDN_SITE_TOKEN not defined' );
}
$return_value = false;
}
if ( false === self::is_cloudways_server() ) {
$return_value = false;
}
return $return_value;
}
/**
* Detect if it's Cloudways server.
*
* @return bool
* @access public
* @since 2.0.19
*/
public static function is_cloudways_server(): bool {
if (
false !== strpos( $_SERVER['DOCUMENT_ROOT'], 'cloudwaysapps' ) ||
false !== strpos( $_SERVER['DOCUMENT_ROOT'], 'cloudwaysstagingapps' ) ||
! empty( getenv( 'FPC_ENV' ) )
) {
return true;
}
return false;
}
public static function is_fmp_server(): bool {
if (
! empty( getenv( 'FPC_ENV' ) ) &&
isset( $_SERVER['HTTP_CF_WORKER'] )
) {
if ( true === self::is_log_enabled() ) {
if ( false !== strpos( getenv( 'FPC_ENV' ), 'uat-' ) ) {
error_log( '# Microservice Server URL UAT: ON ' );
}
if ( false !== strpos( getenv( 'FPC_ENV' ), 'stg-' ) ) {
error_log( '# Microservice Server URL STG: ON ' );
}
if ( false !== strpos( getenv( 'FPC_ENV' ), 'dev-' ) ) {
error_log( '# Microservice Server URL DEV: ON ' );
}
if ( false !== strpos( getenv( 'FPC_ENV' ), 'prod-' ) ) {
error_log( '# Microservice Server URL PROD: ON ' );
}
}
return true;
}
return false;
}
/**
* Detect if it's Cloudways staging server.
*
* @return bool
* @access public
* @since 2.0.19
*/
public static function is_staging_server(): bool {
if (
false !== strpos( $_SERVER['DOCUMENT_ROOT'], 'cloudwaysstagingapps.com' )
) {
if ( true === self::is_log_enabled() ) {
error_log( 'Cloudways Staging ON ' );
}
return true;
}
return false;
}
/**
* Detect if it's Cloudways staging server.
*
* @return bool
* @access public
* @since 2.0.19
*/
public static function is_production_server(): bool {
if (
false !== strpos( $_SERVER['DOCUMENT_ROOT'], 'cloudwaysapps.com' )
) {
if ( true === self::is_log_enabled() ) {
error_log( 'Cloudways Production ON ' );
}
return true;
}
return false;
}
/**
* Check if the server type is FP ( Flexible ).
*
* @return bool
* @since 2.0.15
*/
public static function is_fp_server(): bool {
if ( true === self::is_staging_server() || true === self::is_production_server() ) {
return true;
}
return false;
}
/**
* Spawns a cron job to purge cache asynchronously and triggers it immediately without waiting.
*
* @param array $purge_url_list List of URLs for which to purge cache.
* @param string $endpoint_path Endpoint path to clear URL cache or whole domain cache.
*
* @return void
* @access private
* @since 2.0.15
*/
private function spawn_cron( array $purge_url_list, string $endpoint_path ) {
// Schedule the purge for execution at the current time
wp_schedule_single_event( time() - 1, 'breeze_scheduled_purge', array(
$purge_url_list,
$endpoint_path
) );
$start_cron = spawn_cron();
if ( true === self::is_log_enabled() ) {
error_log( 'CF Purge cron registered!' );
error_log( 'List of URL(s) to be sent: ' . var_export( $purge_url_list, true ) );
error_log( 'Cron started (force): ' . var_export( $start_cron, true ) );
}
}
/**
* Handles the request for purge
*
* @param array $purge_url_list list of URLs for which to purge cache;
* @param string $endpoint_path Endpoint path to clear URL cache or whole domain cache.
* @param string $purge_type Purge type: 'default', 'cron'
*
* @return bool|string|void
* @access private
* @since 2.0.15
*/
private function request_cache_reset( array $purge_url_list = array(), string $endpoint_path = 'purge-fpc-url', string $purge_type = 'default' ) {
if (
false === self::is_cloudflare_enabled() ||
empty( $purge_url_list )
) {
return;
}
self::is_fmp_server();
if ( true === self::is_log_enabled() ) {
error_log( 'CF purge type: ' . var_export( strtoupper( $purge_type ), true ) );
}
if ( 'cron' === $purge_type ) {
$this->spawn_cron( $purge_url_list, $endpoint_path );
return true;
} else {
return $this->execute_purge( $purge_url_list, $endpoint_path );
}
}
/**
* Executes the purge cache request.
*
* @param array $purge_url_list list of URLs for which to purge cache;
* @param string $endpoint_path Endpoint path to clear URL cache or whole domain cache.
*
* @return bool|string
* @access public
* @since 2.0.15
*/
public function execute_purge( array $purge_url_list, string $endpoint_path ) {
// Remove any white spaces from URL list.
$purge_url_list = array_map( 'trim', $purge_url_list );
// Making sure there are no duplicates.
$purge_url_list = array_unique( $purge_url_list );
// Remove empty values.
$purge_url_list = array_values( array_filter( $purge_url_list ) );
if ( empty( $purge_url_list ) ) {
return false;
}
$has_wpml_lang_in_url = $this->purge_urls_have_wpml_language_directory( $purge_url_list );
// Sub-dir purge API expects full URLs with https (domain purge uses scheme-stripped host paths).
if ( 'purge-fpc-sub-dir' === $endpoint_path || $has_wpml_lang_in_url ) {
foreach ( $purge_url_list as &$url ) {
$url = trim( $url );
if ( 0 !== stripos( $url, 'http://' ) && 0 !== stripos( $url, 'https://' ) ) {
$url = 'https://' . ltrim( $url, '/' );
} else {
$url = set_url_scheme( $url, 'https' );
}
}
unset( $url );
}
$verify_host = 2;
$ssl_verification = apply_filters( 'breeze_ssl_check_certificate', true );
if ( ! is_bool( $ssl_verification ) ) {
$ssl_verification = true;
}
if ( defined( 'WP_DEBUG' ) && true === WP_DEBUG ) {
$ssl_verification = false;
$verify_host = 0;
}
// if SSL verification is turned to false then we need to change $verify_host also.
if ( false === $ssl_verification ) {
$verify_host = 0;
}
$rop_user_agent = 'breeze-plugin-cache-reset';
$microservice_url = $this->get_microservice_url();
if ( false === $microservice_url ) {
if ( true === self::is_log_enabled() ) {
error_log( 'Error: Microservice url is not defined ' );
}
return 'baseUrlNotFound';
}
$call_endpoint_url = $microservice_url . $endpoint_path;
// start connection to microservice.
if ( true === self::is_log_enabled() ) {
error_log( '/' . $endpoint_path );
}
$connection = curl_init( $call_endpoint_url );
curl_setopt( $connection, CURLOPT_SSL_VERIFYHOST, $verify_host );
curl_setopt( $connection, CURLOPT_SSL_VERIFYPEER, $ssl_verification );
curl_setopt( $connection, CURLOPT_POST, true );
curl_setopt( $connection, CURLOPT_USERAGENT, $rop_user_agent );
curl_setopt( $connection, CURLOPT_REFERER, home_url() );
// Array to send to microservice.
$data_to_send = array(
'urls' => $purge_url_list,
'appToken' => CDN_SITE_TOKEN,
'appId' => CDN_SITE_ID,
'platform' => $this->cw_platform,
);
if ( true === self::is_log_enabled() ) {
error_log( 'List of URL(s) to be sent: ' . var_export( $data_to_send['urls'], true ) );
error_log( 'Platform used : ' . var_export( strtoupper( $this->cw_platform ), true ) );
}
// Convert data to JSON.
if ( ! empty( $data_to_send ) ) {
$data_to_send = wp_json_encode( $data_to_send );
curl_setopt( $connection, CURLOPT_POSTFIELDS, $data_to_send );
}
// Set request headers.
curl_setopt(
$connection,
CURLOPT_HTTPHEADER,
array(
'Accept: application/json',
'Content-Type: application/json',
'Content-Length: ' . strlen( $data_to_send ),
)
);
/**
* Accept up to 3 maximum redirects before cutting the connection.
*/
curl_setopt( $connection, CURLOPT_MAXREDIRS, 2 );
curl_setopt( $connection, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $connection, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $connection, CURLOPT_TIMEOUT, 30 );
$server_response_body = curl_exec( $connection );
$http_code = curl_getinfo( $connection, CURLINFO_HTTP_CODE );
// Add curl error in logs.
if ( false === $server_response_body ) {
$curl_error = curl_error( $connection );
$curl_errno = curl_errno( $connection );
if ( true === self::is_log_enabled() ) {
error_log( 'cURL Error: ' . $curl_error . ' (Code: ' . $curl_errno . ')' );
}
}
curl_close( $connection );
if ( true === self::is_log_enabled() ) {
error_log( 'Microservice response: ' . var_export( $server_response_body, true ) );
}
return $http_code;
}
/**
* Check if WP_DEBUG is set to true.
* if true then enable logs for this library.
*
* @return bool
*
* @since 2.0.15
* @access public
* @static
*/
public static function is_log_enabled(): bool {
if (
defined( 'BREEZE_CF_DEBUG' ) &&
true === filter_var( BREEZE_CF_DEBUG, FILTER_VALIDATE_BOOLEAN )
) {
return true;
}
return false;
}
}
new Breeze_CloudFlare_Helper();