15, // BypassPermaCache. 'TriggerMatchingType' => 0, // MatchAny. 'Enabled' => true, 'Triggers' => array( array( 'Type' => 3, // UrlExtension. 'PatternMatchingType' => 0, // MatchAny. 'PatternMatches' => array( '.zip' ), ), ), 'Description' => 'Bypass PermaCache for ZIP files', ), array( 'ActionType' => 3, // OverrideCacheTime. 'TriggerMatchingType' => 0, // MatchAny. 'ActionParameter1' => '0', 'ActionParameter2' => '', 'Enabled' => true, 'Triggers' => array( array( 'Type' => 1, // RequestHeader. 'PatternMatchingType' => 0, // MatchAny. 'PatternMatches' => array( '*wordpress_logged_in_*', '*wordpress_sec_*', ), 'Parameter1' => 'Cookie', ), ), 'Description' => 'Override Cache Time if logged into WordPress', ), array( 'ActionType' => 15, // BypassPermaCache. 'TriggerMatchingType' => 0, // MatchAny. 'Enabled' => true, 'Triggers' => array( array( 'Type' => 1, // RequestHeader. 'PatternMatchingType' => 0, // MatchAny. 'PatternMatches' => array( '*wordpress_logged_in_*', '*wordpress_sec_*', ), 'Parameter1' => 'Cookie', ), ), 'Description' => 'Bypass PermaCache if logged into WordPress', ), array( 'ActionType' => 16, // OverrideBrowserCacheTime. 'TriggerMatchingType' => 0, // MatchAny. 'ActionParameter1' => '0', 'Enabled' => true, 'Triggers' => array( array( 'Type' => 1, // RequestHeader. 'PatternMatchingType' => 0, // MatchAny. 'PatternMatches' => array( '*wordpress_logged_in_*', '*wordpress_sec_*', ), 'Parameter1' => 'Cookie', ), ), 'Description' => 'Override Browser Cache Time if logged into WordPress', ), ); /** * Class constructor for initializing API keys and pull zone ID. * * @since 2.6.0 * * @param array $w3tc_config Configuration array containing API keys and pull zone ID. */ public function __construct( array $w3tc_config ) { $this->w3tc_account_api_key = ! empty( $w3tc_config['account_api_key'] ) ? $w3tc_config['account_api_key'] : ''; $this->storage_api_key = ! empty( $w3tc_config['storage_api_key'] ) ? $w3tc_config['storage_api_key'] : ''; $this->stream_api_key = ! empty( $w3tc_config['stream_api_key'] ) ? $w3tc_config['stream_api_key'] : ''; $this->pull_zone_id = ! empty( $w3tc_config['pull_zone_id'] ) ? $w3tc_config['pull_zone_id'] : ''; $this->verify_tls_certificates = self::verify_tls_certificates_enabled( $w3tc_config ); } /** * Whether TLS peer and certificate verification is enabled. * * Missing keys verify. Only an explicit false/0 opt-out disables. * * @since 2.10.6 * * @param array $w3tc_config Client construction array. * * @return bool */ public static function verify_tls_certificates_enabled( array $w3tc_config ) { if ( ! array_key_exists( 'verify_tls_certificates', $w3tc_config ) ) { return true; } return (bool) $w3tc_config['verify_tls_certificates']; } /** * Build a client config from a saved account key plus the matching * Bunny CDN TLS setting. * * @since 2.10.6 * * @param string $account_api_key Account API key. * @param Config $w3tc_config Saved plugin configuration. * @param string $verify_key Config key for the Bunny CDN surface. * * @return array */ public static function client_config_from_saved( $account_api_key, $w3tc_config, $verify_key = 'cdn.bunnycdn.verify_tls_certificates' ) { return array( 'account_api_key' => $account_api_key, 'verify_tls_certificates' => (bool) $w3tc_config->get_boolean( $verify_key, true ), ); } /** * TLS verification for URL purge, from enabled Bunny surfaces. * * Pull-zone IDs on a disabled or non-Bunny surface are ignored. * When both CDN and FSD Bunny engines are enabled, both flags * must be on. * * @since 2.10.6 * * @param Config $w3tc_config Saved plugin configuration. * * @return bool */ public static function url_purge_verify_tls_certificates( $w3tc_config ) { $cdn_active = $w3tc_config->get_boolean( 'cdn.enabled' ) && 'bunnycdn' === $w3tc_config->get_string( 'cdn.engine' ) && $w3tc_config->get_integer( 'cdn.bunnycdn.pull_zone_id' ) > 0; $fsd_active = $w3tc_config->get_boolean( 'cdnfsd.enabled' ) && 'bunnycdn' === $w3tc_config->get_string( 'cdnfsd.engine' ) && $w3tc_config->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' ) > 0; $cdn_verify = (bool) $w3tc_config->get_boolean( 'cdn.bunnycdn.verify_tls_certificates', true ); $fsd_verify = (bool) $w3tc_config->get_boolean( 'cdnfsd.bunnycdn.verify_tls_certificates', true ); if ( $cdn_active && $fsd_active ) { return $cdn_verify && $fsd_verify; } if ( $fsd_active ) { return $fsd_verify; } if ( $cdn_active ) { return $cdn_verify; } return true; } /** * Filters the timeout time. * * @since 2.6.0 * * @param int $time The original timeout time. * * @return int The adjusted timeout time. */ public function filter_timeout_time( $time ) { return 600; } /** * Filters whether TLS certificates are verified for HTTPS requests. * * Honors the Bunny CDN compatibility opt-out. Defaults to verifying. * * @since 2.6.0 * * @param bool $verify Whether to enable SSL verification. * * @return bool */ public function https_ssl_verify( $verify = true ) { return $this->verify_tls_certificates; } /** * Lists all pull zones. * * @since 2.6.0 * * @link https://docs.bunny.net/reference/pullzonepublic_index * * @return array|WP_Error API response or error object. */ public function list_pull_zones() { $this->api_type = 'account'; return $this->wp_remote_get( \esc_url( 'https://api.bunny.net/pullzone' ) ); } /** * Gets the details of a specific pull zone. * * @since 2.6.0 * * @param int $id The pull zone ID. * * @link https://docs.bunny.net/reference/pullzonepublic_index2 * * @return array|WP_Error API response or error object. */ public function get_pull_zone( $id ) { $this->api_type = 'account'; return $this->wp_remote_get( \esc_url( 'https://api.bunny.net/pullzone/id' . $id ) ); } /** * Adds a new pull zone. * * @since 2.6.0 * * @param array $w3tc_data Data for the new pull zone. * * @link https://docs.bunny.net/reference/pullzonepublic_add * * @return array|WP_Error API response or error object. * * @throws \Exception If the pull zone name is invalid. */ public function add_pull_zone( array $w3tc_data ) { $this->api_type = 'account'; if ( empty( $w3tc_data['Name'] ) || ! \is_string( $w3tc_data['Name'] ) ) { // A Name string is required, which is used for the CDN hostname. throw new \Exception( \esc_html__( 'A pull zone name (string) is required.', 'w3-total-cache' ) ); } if ( \preg_match( '[^\w\d-]', $w3tc_data['Name'] ) ) { // Only letters, numbers, and dashes are allowed in the Name. throw new \Exception( \esc_html__( 'A pull zone name (string) is required.', 'w3-total-cache' ) ); } return $this->wp_remote_post( 'https://api.bunny.net/pullzone', $w3tc_data ); } /** * Updates an existing pull zone. * * @since 2.6.0 * * @param int $id The pull zone ID. * @param array $w3tc_data Data for updating the pull zone. * * @link https://docs.bunny.net/reference/pullzonepublic_updatepullzone * * @return array|WP_Error API response or error object. * * @throws \Exception If the pull zone ID is invalid. */ public function update_pull_zone( $id, array $w3tc_data ) { $this->api_type = 'account'; $id = empty( $this->pull_zone_id ) ? $id : $this->pull_zone_id; if ( empty( $id ) || ! \is_int( $id ) ) { throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) ); } return $this->wp_remote_post( 'https://api.bunny.net/pullzone/' . $id, $w3tc_data ); } /** * Deletes a pull zone. * * @since 2.6.0 * * @param int $id The pull zone ID. * * @link https://docs.bunny.net/reference/pullzonepublic_delete * * @return array|WP_Error API response or error object. * * @throws \Exception If the pull zone ID is invalid. */ public function delete_pull_zone( $id ) { $this->api_type = 'account'; $id = empty( $this->pull_zone_id ) ? $id : $this->pull_zone_id; if ( empty( $id ) || ! \is_int( $id ) ) { throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) ); } return $this->wp_remote_post( \esc_url( 'https://api.bunny.net/pullzone/' . $id ), array(), array( 'method' => 'DELETE' ) ); } /** * Adds a custom hostname to a pull zone. * * @since 2.6.0 * * @param string $w3tc_hostname The custom hostname to add. * @param int|null $pull_zone_id The pull zone ID (optional). * * @link https://docs.bunny.net/reference/pullzonepublic_addhostname * * @return void * * @throws \Exception If the pull zone ID or hostname is invalid. */ public function add_custom_hostname( $w3tc_hostname, $pull_zone_id = null ) { $this->api_type = 'account'; $pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id; if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) { throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) ); } if ( empty( $w3tc_hostname ) || ! \filter_var( $w3tc_hostname, FILTER_VALIDATE_DOMAIN ) ) { throw new \Exception( \esc_html__( 'Invalid hostname', 'w3-total-cache' ) . ' "' . \esc_html( $w3tc_hostname ) . '".' ); } $this->wp_remote_post( \esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/addHostname' ), array( 'Hostname' => $w3tc_hostname ) ); } /** * Gets the default edge rules for the pull zone. * * @since 2.6.0 * * @return array Default edge rules. */ public static function get_default_edge_rules() { return self::$default_edge_rules; } /** * Adds an edge rule to a pull zone. * * @since 2.6.0 * * @param array $w3tc_data Data for the edge rule. * @param int|null $pull_zone_id The pull zone ID (optional). * * @return void * * @throws \Exception If any required parameters are missing or invalid. */ public function add_edge_rule( array $w3tc_data, $pull_zone_id = null ) { $this->api_type = 'account'; $pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id; if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) { throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) ); } if ( ! isset( $w3tc_data['ActionType'] ) || ! \is_int( $w3tc_data['ActionType'] ) || $w3tc_data['ActionType'] < 0 ) { throw new \Exception( \esc_html__( 'Invalid parameter "ActionType".', 'w3-total-cache' ) ); } if ( ! isset( $w3tc_data['TriggerMatchingType'] ) || ! \is_int( $w3tc_data['TriggerMatchingType'] ) || $w3tc_data['TriggerMatchingType'] < 0 ) { throw new \Exception( \esc_html__( 'Invalid parameter "TriggerMatchingType".', 'w3-total-cache' ) ); } if ( ! isset( $w3tc_data['Enabled'] ) || ! \is_bool( $w3tc_data['Enabled'] ) ) { throw new \Exception( \esc_html__( 'Missing parameter "Enabled".', 'w3-total-cache' ) ); } if ( empty( $w3tc_data['Triggers'] ) ) { throw new \Exception( \esc_html__( 'Missing parameter "Triggers".', 'w3-total-cache' ) ); } $this->wp_remote_post( \esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/edgerules/addOrUpdate' ), $w3tc_data ); } /** * Purges the cache. * * @since 2.6.0 * * @param array $w3tc_data Data for the purge operation. * * @return array|WP_Error API response or error object. */ public function purge( array $w3tc_data ) { $this->api_type = 'account'; return $this->wp_remote_post( \esc_url( 'https://api.bunny.net/purge' ), $w3tc_data ); } /** * Purges the cache for a specific pull zone. * * @since 2.6.0 * * @param int|null $pull_zone_id The pull zone ID (optional). * * @return void * * @throws \Exception If the pull zone ID is invalid. */ public function purge_pull_zone( $pull_zone_id = null ) { $this->api_type = 'account'; $pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id; if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) { throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) ); } $this->wp_remote_post( \esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/purgeCache' ) ); } /** * Retrieves the appropriate API key based on the specified type. * * @since 2.6.0 * * @param string|null $type The type of API key to retrieve ('account', 'storage', or 'stream'). * * @return string The API key. * * @throws \Exception If the API key type is invalid or the key is empty. */ private function get_api_key( $type = null ) { if ( empty( $type ) ) { $type = $this->api_type; } if ( ! \in_array( $type, array( 'account', 'storage', 'stream' ), true ) ) { throw new \Exception( \esc_html__( 'Invalid API type; must be one of "account", "storage", "stream".', 'w3-total-cache' ) ); } $w3tc_api_keys = array( 'account' => $this->w3tc_account_api_key, 'storage' => $this->storage_api_key, 'stream' => $this->stream_api_key, ); if ( empty( $w3tc_api_keys[ $type ] ) ) { throw new \Exception( \esc_html__( 'API key value is empty.', 'w3-total-cache' ) ); } return $w3tc_api_keys[ $type ]; } /** * Decodes the API response. * * @since 2.6.0 * * @param array|WP_Error $w3tc_result The result returned from the API request. * * @return array The decoded response data. * * @throws \Exception If the response is not successful or fails to decode. */ private function decode_response( $w3tc_result ) { if ( \is_wp_error( $w3tc_result ) ) { throw new \Exception( \esc_html( sprintf( /* translators: %s: transport error from the HTTP API */ \__( 'Failed to reach API endpoint: %s', 'w3-total-cache' ), $w3tc_result->get_error_message() ) ) ); } $response_body = @\json_decode( $w3tc_result['body'], true ); // Throw an exception if the response code/status is not ok. if ( ! \in_array( $w3tc_result['response']['code'], array( 200, 201, 204 ), true ) ) { $w3tc_message = isset( $response_body['Message'] ) ? $response_body['Message'] : $w3tc_result['body']; throw new \Exception( \esc_html( \__( 'Response code ', 'w3-total-cache' ) . $w3tc_result['response']['code'] . ': ' . $w3tc_message ) ); } return \is_array( $response_body ) ? $response_body : array(); } /** * Sends a GET request to a specified URL with optional data parameters. * * This method sends a GET request using `wp_remote_get` to the specified URL, including optional query parameters. * It also adds custom headers for API authentication and content type. Timeout and SSL verification filters * are applied during the request process. The response is processed using `decode_response` method. * * @since 2.6.0 * * @param string $w3tc_url The URL to send the GET request to. * @param array $w3tc_data Optional. An associative array of data to send as query parameters. Default is an empty array. * * @return mixed The decoded response from the API request. */ private function wp_remote_get( $w3tc_url, array $w3tc_data = array() ) { $api_key = $this->get_api_key(); \add_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) ); \add_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) ); $w3tc_result = \wp_remote_get( $w3tc_url . ( empty( $w3tc_data ) ? '' : '?' . \http_build_query( $w3tc_data ) ), array( 'headers' => array( 'AccessKey' => $api_key, 'Accept' => 'application/json', ), 'sslverify' => $this->verify_tls_certificates, ) ); \remove_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) ); \remove_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) ); return self::decode_response( $w3tc_result ); } /** * Sends a POST request to a specified URL with optional data and additional arguments. * * This method sends a POST request using `wp_remote_post` to the specified URL, including optional data in the request body * and additional arguments. Custom headers for API authentication, content type, and accept type are included in the request. * Filters for request timeout and SSL verification are applied during the request process. The response is processed using * `decode_response` method. * * @since 2.6.0 * * @param string $w3tc_url The URL to send the POST request to. * @param array $w3tc_data Optional. An associative array of data to send in the request body. Default is an empty array. * @param array $args Optional. Additional arguments to customize the POST request, such as custom headers or settings. Default is an empty array. * * @return mixed The decoded response from the API request. */ private function wp_remote_post( $w3tc_url, array $w3tc_data = array(), array $args = array() ) { $api_key = $this->get_api_key(); \add_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) ); \add_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) ); $w3tc_result = \wp_remote_post( $w3tc_url, \array_merge( array( 'headers' => array( 'AccessKey' => $api_key, 'Accept' => 'application/json', 'Content-Type' => 'application/json', ), 'body' => empty( $w3tc_data ) ? null : \wp_json_encode( $w3tc_data ), ), $args, array( 'sslverify' => $this->verify_tls_certificates, ) ) ); \remove_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) ); \remove_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) ); return self::decode_response( $w3tc_result ); } }