';
}
/**
* Schedule a check in about a minute (after activation / update / settings save).
*/
public static function schedule_soon() {
if ( ! wp_next_scheduled( self::ONCE_HOOK ) ) {
wp_schedule_single_event( time() + MINUTE_IN_SECONDS, self::ONCE_HOOK );
}
}
/**
* Last stored result.
*
* @return array|null
*/
public static function result() {
$r = get_option( self::OPTION );
return is_array( $r ) ? $r : null;
}
/**
* Run the check and store the result.
*
* @return array
*/
public static function run() {
$page_id = (int) Settings::get( 'page_id' );
$url = $page_id ? get_permalink( $page_id ) : '';
if ( ! $url || 'publish' !== get_post_status( $page_id ) ) {
$result = array(
'time' => time(),
'url' => '',
'bare' => 'error',
'query' => 'error',
'detected' => array(),
'error' => __( 'The form page is missing or not published.', 'otkazpro' ),
);
update_option( self::OPTION, $result, false );
return $result;
}
$bare = self::probe( $url );
$query = self::probe( add_query_arg( 'otkazpro_done', 'cachecheck', $url ) );
$detected = array_merge( $bare['detected'], $query['detected'] );
if ( 'cached' === $bare['status'] || 'cached' === $query['status'] ) {
$detected = array_merge( $detected, self::active_cache_plugins() );
}
$result = array(
'time' => time(),
'url' => $url,
'bare' => $bare['status'],
'query' => $query['status'],
'detected' => array_values( array_unique( $detected ) ),
'error' => $bare['error'] ? $bare['error'] : $query['error'],
);
update_option( self::OPTION, $result, false );
/**
* Fires after the cache self-check.
*
* @param array $result Result.
*/
do_action( 'py_otkaz_cache_checked', $result );
return $result;
}
/**
* Load a URL twice as an anonymous visitor and compare the render markers.
*
* @param string $url URL.
* @return array status ok|cached|error, detected[], error.
*/
private static function probe( $url ) {
$markers = array();
$detected = array();
for ( $i = 0; $i < 2; $i++ ) {
$response = self::fetch( $url );
if ( is_wp_error( $response ) ) {
return array(
'status' => 'error',
'detected' => $detected,
'error' => $response->get_error_message(),
);
}
$code = (int) wp_remote_retrieve_response_code( $response );
$body = (string) wp_remote_retrieve_body( $response );
$detected = array_merge( $detected, self::detect_from_response( $response, $body ) );
if ( 200 !== $code ) {
return array(
'status' => 'error',
/* translators: %d: HTTP status code */
'error' => sprintf( __( 'The form page returned HTTP %d to the check (firewall or bot protection?).', 'otkazpro' ), $code ),
'detected' => $detected,
);
}
if ( preg_match( '/data-otkazpro-render="([A-Za-z0-9]+)"/', $body, $m ) || preg_match( '/' . preg_quote( self::MARKER_KEY, '/' ) . '([A-Za-z0-9]+)/', $body, $m ) ) {
$markers[] = $m[1];
continue;
}
if ( preg_match( '/data-(otkazpro|wpotkaz-otkaz|py-otkaz)[\s=>"]/', $body ) ) {
// The form is there but without a render marker: an old cached copy
// (made before the marker existed) – i.e. the page is served from cache.
return array(
'status' => 'cached',
'detected' => array_values( array_unique( $detected ) ),
'error' => '',
);
}
return array(
'status' => 'error',
'error' => __( 'The form was not found on the page (is the shortcode on the selected page?).', 'otkazpro' ),
'detected' => $detected,
);
}
return array(
'status' => $markers[0] === $markers[1] ? 'cached' : 'ok',
'detected' => $markers[0] === $markers[1] ? array_values( array_unique( $detected ) ) : array(),
'error' => '',
);
}
/**
* GET as an anonymous visitor, one retry on transport errors.
*
* @param string $url URL.
* @return array|\WP_Error
*/
private static function fetch( $url ) {
$args = array(
'timeout' => 20,
'redirection' => 3,
'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- third-party / core hook.
'headers' => array(
// A regular visitor – some caches skip bots.
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36',
'Accept' => 'text/html,application/xhtml+xml',
'Accept-Language' => 'bg-BG,bg;q=0.9,en;q=0.8',
),
);
$response = wp_remote_get( $url, $args );
if ( is_wp_error( $response ) ) {
sleep( 1 );
$response = wp_remote_get( $url, $args );
}
return $response;
}
/**
* Cache layer names recognisable from response headers / HTML.
*
* @param array|\WP_HTTP_Response $response Response.
* @param string $body Body.
* @return string[]
*/
private static function detect_from_response( $response, $body ) {
$found = array();
$h = static function ( $name ) use ( $response ) {
return strtolower( (string) wp_remote_retrieve_header( $response, $name ) );
};
if ( '' !== $h( 'x-flying-press-cache' ) || false !== stripos( $body, 'Powered by FlyingPress' ) ) {
$found[] = 'FlyingPress';
}
if ( '' !== $h( 'x-litespeed-cache' ) ) {
$found[] = 'LiteSpeed Cache';
}
if ( false !== stripos( $body, 'This website is like a Rocket' ) ) {
$found[] = 'WP Rocket';
}
if ( false !== stripos( $body, 'Performance optimized by W3 Total Cache' ) && false === stripos( $body, 'DONOTCACHEPAGE' ) ) {
$found[] = 'W3 Total Cache';
}
if ( false !== stripos( $body, 'WP Fastest Cache' ) ) {
$found[] = 'WP Fastest Cache';
}
if ( in_array( $h( 'cf-cache-status' ), array( 'hit', 'stale', 'revalidated', 'updating' ), true ) ) {
$found[] = 'Cloudflare';
}
if ( '' !== $h( 'x-kinsta-cache' ) ) {
$found[] = 'Kinsta (server cache)';
}
if ( '' !== $h( 'x-sg-cache' ) || ( '' !== $h( 'x-proxy-cache' ) && false !== strpos( $h( 'x-proxy-cache' ), 'hit' ) ) ) {
$found[] = 'Hosting proxy cache (SiteGround / Nginx)';
}
if ( '' !== $h( 'x-varnish' ) ) {
$found[] = 'Varnish';
}
if ( '' !== $h( 'x-sucuri-cache' ) ) {
$found[] = 'Sucuri';
}
return $found;
}
/**
* Active page-cache plugins (to name them in the instructions).
*
* @return string[]
*/
private static function active_cache_plugins() {
$map = array(
'WP Rocket' => defined( 'WP_ROCKET_VERSION' ),
'LiteSpeed Cache' => defined( 'LSCWP_V' ),
'W3 Total Cache' => defined( 'W3TC' ),
'WP Super Cache' => defined( 'WPCACHEHOME' ),
'WP Fastest Cache' => class_exists( 'WpFastestCache' ),
'FlyingPress' => defined( 'FLYING_PRESS_VERSION' ) || class_exists( 'FlyingPress\\Caching' ),
'SiteGround Speed Optimizer' => class_exists( 'SiteGround_Optimizer\\Loader\\Loader' ),
'Breeze' => defined( 'BREEZE_VERSION' ),
'NitroPack' => defined( 'NITROPACK_VERSION' ),
'Hummingbird' => defined( 'WPHB_VERSION' ),
'Cache Enabler' => class_exists( 'Cache_Enabler' ),
);
return array_keys( array_filter( $map ) );
}
/**
* Overall status: good | recommended | critical | unknown.
*
* @param array|null $r Result.
* @return string
*/
public static function level( $r ) {
if ( ! $r ) {
return 'unknown';
}
if ( 'cached' === $r['query'] ) {
return 'critical';
}
if ( 'cached' === $r['bare'] ) {
return 'recommended';
}
if ( 'error' === $r['bare'] || 'error' === $r['query'] ) {
return 'unknown';
}
return 'good';
}
/**
* Human-readable summary + instructions (HTML).
*
* @param array|null $r Result.
* @return string
*/
public static function describe( $r ) {
$level = self::level( $r );
if ( ! $r ) {
return esc_html__( 'The cache check has not run yet.', 'otkazpro' );
}
$names = $r['detected'] ? implode( ', ', $r['detected'] ) : __( 'a page cache (plugin, hosting or CDN)', 'otkazpro' );
$path = $r['url'] ? (string) wp_parse_url( $r['url'], PHP_URL_PATH ) : '/otkaz-ot-dogovor/';
if ( $r['url'] && ( '/' === $path || '' === $path ) ) {
$path = $r['url']; // Plain permalinks (?page_id=…): show the full address.
}
$fix = sprintf(
/* translators: 1: cache names, 2: page path */
__( 'Exclude %2$s and everything after it from caching in %1$s (for Cloudflare: a Cache Rule “Bypass cache” for this path), purge the cache and run the check again.', 'otkazpro' ),
$names,
$path
);
switch ( $level ) {
case 'critical':
return '' . esc_html__( 'Pages with a query string are served from cache: a customer’s order page could be shown to other visitors.', 'otkazpro' ) . ' ' . esc_html( $fix );
case 'recommended':
return esc_html__( 'The form page is served from cache. The form keeps working, but excluding it is recommended.', 'otkazpro' ) . ' ' . esc_html( $fix );
case 'good':
return esc_html__( 'The form page is not cached.', 'otkazpro' );
default:
return esc_html__( 'The check could not be completed:', 'otkazpro' ) . ' ' . esc_html( $r['error'] );
}
}
/**
* "Check cache" button.
*/
public static function handle_button() {
check_admin_referer( 'otkazpro_cache_check' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to do this.', 'otkazpro' ) );
}
self::run();
wp_safe_redirect( add_query_arg( 'otkazpro_msg', 'cache_checked', Settings::url() ) );
exit;
}
/**
* Button URL.
*
* @return string
*/
public static function button_url() {
return wp_nonce_url( admin_url( 'admin-post.php?action=otkazpro_cache_check' ), 'otkazpro_cache_check' );
}
/**
* Register the Site Health test.
*
* @param array $tests Tests.
* @return array
*/
public static function register_site_health( $tests ) {
$tests['direct']['otkazpro_cache'] = array(
'label' => __( 'OtkazPro: withdrawal form is not cached', 'otkazpro' ),
'test' => array( __CLASS__, 'site_health_result' ),
);
return $tests;
}
/**
* Site Health result (from the stored check – no live requests here).
*
* @return array
*/
public static function site_health_result() {
$r = self::result();
$level = self::level( $r );
$status = in_array( $level, array( 'good', 'recommended', 'critical' ), true ) ? $level : 'recommended';
$labels = array(
'good' => __( 'The withdrawal form page is not cached', 'otkazpro' ),
'recommended' => __( 'The withdrawal form page is cached or could not be checked', 'otkazpro' ),
'critical' => __( 'Customer order pages of the withdrawal form are cached', 'otkazpro' ),
);
return array(
'label' => $labels[ $status ],
'status' => $status,
'badge' => array(
'label' => 'OtkazPro',
'color' => 'critical' === $status ? 'red' : ( 'good' === $status ? 'blue' : 'orange' ),
),
'description' => '' . self::describe( $r ) . '
',
'actions' => sprintf( '%s
', esc_url( Settings::url() ), esc_html__( 'Open OtkazPro settings', 'otkazpro' ) ),
'test' => 'otkazpro_cache',
);
}
}