log_sp_action(get_current_user_id(), sprintf('Platform flush cache call. type: %s', $method) ); $this->executeRequest($method, $host, $urlHttp); $this->executeRequest($method, $host, $urlHttps); } private function executeRequest($method, $host, $url) { wp_remote_request( esc_url_raw($url), [ 'method' => $method, 'blocking' => false, 'headers' => [ 'Host' => $host, ], ] ); } /** * Initialize script. * * @action init */ public function init() { $action = filter_input( INPUT_GET, 'wpaas_action' ); $nonce = filter_input( INPUT_GET, 'wpaas_nonce' ); if ( ! current_user_can( self::$cap ) || 'flush_cache' !== $action || false === wp_verify_nonce( $nonce, 'wpaas_flush_cache' ) ) { return; } if ( ! $this->is_token_under_limit( self::CACHE_BAN_KEY, self::MAX_BAN_LIMIT, self::MAX_BAN_LIMIT_TTL ) ) { Admin\Growl::add( __( 'You have exceeded the maximum number of cache flushes. Please wait 5 minutes', 'gd-system-plugin' ) ); wp_safe_redirect( esc_url_raw( remove_query_arg( [ 'GD_COMMAND', // Backwards compat 'wpaas_action', 'wpaas_nonce', ] ) ) ); exit; } $this->do_ban(); $this->flush_transients(); $this->flush_ob(); $cdn_full_page = defined('GD_CDN_FULLPAGE') ? GD_CDN_FULLPAGE : false; if ($cdn_full_page ) { Admin\Growl::add( __( 'Clear cache in progress', 'gd-system-plugin' ) ); } else { Admin\Growl::add( __( 'Cache cleared', 'gd-system-plugin' ) ); } wp_safe_redirect( esc_url_raw( remove_query_arg( [ 'GD_COMMAND', // Backwards compat 'wpaas_action', 'wpaas_nonce', ] ) ) ); exit; } /** * Flush cache on shutdown when certain options are updated. * * @action update_option * * @param string $option */ public function update_option( $option ) { $options = [ 'avatar_default', 'blogdescription', 'blogname', 'category_base', 'close_comments_days_old', 'close_comments_for_old_posts', 'comment_order', 'comment_registration', 'comments_per_page', 'date_format', 'default_comments_page', 'gmt_offset', 'page_comments', 'page_for_posts', 'page_on_front', 'permalink_structure', 'rewrite_rules', 'posts_per_page', 'require_name_email', 'show_avatars', 'sidebars_widgets', 'start_of_week', 'tag_base', 'thread_comments', 'thread_comments_depth', 'time_format', 'timezone_string', 'WPLANG', "siteurl", "home", ]; if ( in_array( $option, $options, true ) || 0 === strpos( $option, 'widget_' ) || 0 === strpos( $option, 'theme_mods_' ) ) { $limit = self::MAX_OPTION_BAN_LIMIT; if ( get_current_user_id() > 0 ){ $limit = $limit * 3; } if ( $this->is_token_under_limit(self::CACHE_BAN_OPTIONS_KEY, $limit, self::MAX_OPTION_BAN_TTL) ) { $this->increment_token(self::CACHE_BAN_OPTIONS_KEY, $limit, self::MAX_OPTION_BAN_TTL); $this->do_ban(); } } } /** * Set to ban cache on shutdown. */ public function do_ban() { if ( $this->has_ban() ) { return; } remove_action( 'shutdown', [ $GLOBALS['wpaas_cache_class'], 'purge' ], PHP_INT_MAX ); add_action( 'shutdown', [ $GLOBALS['wpaas_cache_class'], 'ban' ], PHP_INT_MAX ); } /** * Set purge URLs and set to purge cache on shutdown. * * @param int $id * @param \WP_Post $post (optional) */ public function do_purge( $id, $post = null ) { if ( $this->has_ban() ) { return; } if ( ! is_a( $post, 'WP_Post' ) ) { // Assume anything that isn't a post is a comment $comment = get_comment( $id ); if ( ! is_a( $comment, 'WP_Comment' ) ) { return; } $post = get_post( $comment->comment_post_ID ); } if ( wp_is_post_revision( $post ) ) { return; } if( 'auto-draft' === get_post_status($post) && $GLOBALS['wpaas_feature_flag']->get_feature_flag_value('persist-autodraft-posts', false) ) { return; } /** * Purge all URLs where a post might appear */ self::$purge_urls[] = trailingslashit( home_url() ); self::$purge_urls[] = get_permalink( $post->ID ); $post_archive_link = get_post_type_archive_link( $post->post_type ); if ( $post_archive_link != get_home_url() ) { // omit archive link that is same as homepage self::$purge_urls[] = $post_archive_link; } self::$purge_urls[] = get_post_type_archive_feed_link( $post->post_type ); self::$purge_urls[] = get_author_posts_url( (int) $post->post_author ); // Taxonomy-related URLs foreach ( get_post_taxonomies( $post ) as $tax ) { $post_terms = wp_get_post_terms( $post->ID, $tax ); if ( is_wp_error( $post_terms ) ) { continue; } foreach ( $post_terms as $term ) { self::$purge_urls[] = get_term_link( $term ); self::$purge_urls[] = get_term_feed_link( $term->term_id, $term->taxonomy ); } } foreach ( self::$purge_urls as $key => $url ) { // Archive page might return false if ( ! $url || is_wp_error( $url ) ) { unset( self::$purge_urls[ $key ] ); } } self::$purge_urls = array_values( array_unique( self::$purge_urls ) ); if ($this->should_switch_to_ban()) { $this->do_ban(); return; } if ( ! $this->has_purge() ) { add_action( 'shutdown', [ $GLOBALS['wpaas_cache_class'], 'purge' ], PHP_INT_MAX ); } } /** * Delete all transient data from the options table * * WordPress only deletes expired transients when something tries * to call that transient key again. This means over time there could * be many thousands of transient option rows polluting the database, * which can result in noticable performance impact. * * This method should be called when the customer is explicitly * clearing their site's cache. Since transients are a form of cache, * we will flush them all away regardless of TTL status. * * @see HOSTAPPS-3157/WPDEV-708 * * @return int|false Number of rows affected/selected or false on error. */ public function flush_transients() { global $wpdb; return $wpdb->query( "DELETE FROM `{$wpdb->options}` WHERE `option_name` LIKE '%_transient_%';" ); } /** * Clear OBP cache even when nocache=1 is used. * * @see VOICEIT-9348 * * @return void */ public function flush_ob() { if (isset($GLOBALS['ObjectCachePro'])) { wp_cache_flush(); } } /** * Return a nonced flush cache URL. * * @return string */ public static function get_flush_url() { return esc_url( add_query_arg( [ 'wpaas_action' => 'flush_cache', 'wpaas_nonce' => wp_create_nonce( 'wpaas_flush_cache' ), ] ) ); } /** * Perform CDN flush * * @return void */ public function flush_cdn() { if ( defined('GD_CDN_FULLPAGE') && true === GD_CDN_FULLPAGE ) { $invalidation_id = self::$api->flush_cdn(); if ( $invalidation_id ) { update_option( 'gd_system_polling_invalidation_id', $invalidation_id ); } /** Logg user action */ $log_message = 'CDN flush cache call. status: '; if (is_null($invalidation_id)) { $log_message .= 'FAIL'; } else { $log_message .= 'SUCCESS invalidation_id: '.$invalidation_id; } $GLOBALS['wpaas_activity_logger']->log_sp_action(get_current_user_id(), $log_message ); return $invalidation_id; } } public function flush_cdn_polling_script( ) { if ( get_option('gd_system_polling_invalidation_id') ) { wp_enqueue_script( 'wpaas-flush-cdn-polling', Plugin::assets_url( 'js/flush-cdn-status-polling.js' ), [ 'jquery' ] ); wp_localize_script( 'wpaas-flush-cdn-polling' , 'wpaas_flush_cdn_polling_object', [ 'ajaxUrl' => esc_url_raw( rest_url() ) . 'wpaas/v1/flush-cache/status', 'nonce' => wp_create_nonce( 'wp_rest' ) ]); } } /** * Send request to web tier to flush OBP cache on CLI action * * @see MWPPLAT-3388 * @return void */ public function web_flush_cache() { $domain = defined( 'GD_TEMP_DOMAIN' ) ? GD_TEMP_DOMAIN : null; if ( !$domain ) { return; } /** Logg user action */ $GLOBALS['wpaas_activity_logger']->log_sp_action(get_current_user_id(), 'Flush cache call from CLI' ); $api_url = sprintf('https://%s/wp-json/wpaas/v1/flush-cache', $domain); $data = Plugin::sign_http_request(wp_json_encode( [] )); $headers = []; foreach ($data as $key => $item) { $headers[str_replace('wp-', '', $key)] = $item; } wp_remote_request( esc_url_raw( $api_url ), [ 'method' => 'POST', 'blocking' => true, 'headers' => array_merge( [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], $headers ), ] ); } /** * Check if a BAN request is already set to fire on shutdown. * * @return bool */ public function has_ban() { return has_action( 'shutdown', [ $GLOBALS['wpaas_cache_class'], 'ban' ] ); } /** * Check if a PURGE request is already set to fire on shutdown. * * @return bool */ public function has_purge() { return has_action( 'shutdown', [ $GLOBALS['wpaas_cache_class'], 'purge' ] ); } /** * Ban all cache (async). * * @return bool */ public function ban() { if ( 'shutdown' !== current_action() ) { return false; } if ( ! $this->is_token_under_limit( self::CACHE_BAN_KEY, self::MAX_BAN_LIMIT, self::MAX_BAN_LIMIT_TTL ) ) { /** Logg user action */ $GLOBALS['wpaas_activity_logger']->log_sp_action(get_current_user_id(), 'Flush cache, CDN cache flush throttled' ); return false; } $this->increment_token( self::CACHE_BAN_KEY, self::MAX_BAN_LIMIT, self::MAX_BAN_LIMIT_TTL ); $this->request( 'BAN' ); if ( Plugin::is_wp_cli() ) { $this->web_flush_cache(); } $this->flush_cdn(); /** * Fires after all site cache has been banned. * * @since 2.0.1 */ do_action( 'wpaas_cache_banned' ); return true; } /** * Purge the Varnish cache selectively (async). * * @param array $urls (optional) * * @return bool */ public function purge( $urls = [] ) { if ( 'shutdown' !== current_action() ) { return false; } $urls = ( $urls ) ? $urls : self::$purge_urls; if ( ! $urls ) { return false; } $urls = array_unique( $urls ); foreach ( $urls as $url ) { $this->request( 'PURGE', $url ); } /** * Fires after cache has been purged on specific URLs. * * @since 2.0.1 * * @param array $urls */ do_action( 'wpaas_cache_purged', $urls ); return true; } /** * Propogate nocache call to scripts and styles. * * When the `nocache` query arg is being used in the page * request we need to ensure that any scripts and styles * from this domain being called also use it. * * @filter script_loader_src * @filter style_loader_src * * @param string $src * * @return string */ public function nocache( $src ) { $is_external = ( false === stripos( $src, Plugin::domain() ) ); $is_nocache = ( false !== stripos( filter_input( INPUT_SERVER, 'QUERY_STRING' ), 'nocache' ) ); if ( ! $is_external && $is_nocache ) { $src = add_query_arg( 'nocache', 1, $src ); } return $src; } /** * * @return bool */ private function should_switch_to_ban() { $cdn_full_page = defined('GD_CDN_FULLPAGE') ? GD_CDN_FULLPAGE : false; if ( $cdn_full_page || count(self::$purge_urls) > self::MAX_PURGE_URLS ) { return true; } return false; } /** * Return true if specific token key is under limit and actions is allowed * * @param string $token * @param int $limit * @param int $ttl * @return bool */ private function is_token_under_limit( $token, $limit, $ttl ) { $cutoff_time = time() - $ttl; $value = get_option( $token, false ); if ( $value === false ) { return true; } if ( ! is_array( $value ) ) { $value = []; } $counter = 0; foreach ( $value as $v ) { if ( $v > $cutoff_time ) { $counter ++; } } return $counter < $limit; } /** * Increment specific token counter * * @param string $token * @param int $limit * @param int $ttl * @return void */ private function increment_token( $token, $limit, $ttl ) { $insert = false; $current_time = time(); $cutoff_time = time() - $ttl; $value = get_option( $token, false ); if ( $value === false ) { $value = []; $insert = true; } if ( is_array( $value ) === false ) { $value = []; } $new_array = []; foreach ( $value as $v ) { if ( $v > $cutoff_time ) { $new_array[] = $v; } } if ( count( $new_array ) < $limit ) { $new_array[] = $current_time; } $this->upsert( $token, $new_array, $insert ); } /** * @param string $key * @param mixed $value * @param bool $insert * @return void */ private function upsert( $key, $value, $insert ) { if ( $insert ) { add_option( $key, $value ); } else { update_option( $key, $value ); } } }