✨feature: Add reset-cache action to settings page
Adds a 'Reset cache' button to the Podcast RSS Import settings page that clears the SimplePie feed-cache rows (both the cached XML and the modification timestamp) plus the importer's per-feed import lock for the configured feed. Multisite site-transient variants are also cleared. Use this when the importer is skipping items or refusing to re-fetch a feed that has changed at the source. The next 'Import now' click will re-pull the XML from the network. clear_feed_cache() is exposed as a public method so it is unit-testable, and a do_action hook (rss2cpt_cache_cleared) lets other code react when the cache is cleared. Two smoke tests pin down the expected option and site-transient names, and a third ensures an empty feed URL is a no-op.
This commit is contained in:
@@ -31,9 +31,15 @@ final class Settings {
|
||||
/** Manual import admin-post action. */
|
||||
public const MANUAL_IMPORT_ACTION = 'rss2cpt_manual_import';
|
||||
|
||||
/** Cache-reset admin-post action. */
|
||||
public const RESET_CACHE_ACTION = 'rss2cpt_reset_cache';
|
||||
|
||||
/** Manual import nonce action. */
|
||||
private const NONCE_ACTION = 'rss2cpt_run_manual_import';
|
||||
|
||||
/** Cache-reset nonce action. */
|
||||
private const RESET_NONCE_ACTION = 'rss2cpt_reset_cache';
|
||||
|
||||
/**
|
||||
* Import service.
|
||||
*
|
||||
@@ -71,6 +77,7 @@ final class Settings {
|
||||
add_action( 'admin_menu', array( $this, 'register_page' ) );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_action( 'admin_post_' . self::MANUAL_IMPORT_ACTION, array( $this, 'handle_manual_import' ) );
|
||||
add_action( 'admin_post_' . self::RESET_CACHE_ACTION, array( $this, 'handle_reset_cache' ) );
|
||||
add_action( 'admin_notices', array( $this, 'render_notices' ) );
|
||||
$this->scheduler->ensure_scheduled();
|
||||
}
|
||||
@@ -233,6 +240,14 @@ final class Settings {
|
||||
<?php wp_nonce_field( self::NONCE_ACTION ); ?>
|
||||
<?php submit_button( __( 'Import now', 'rss2cpt' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
|
||||
<h2><?php esc_html_e( 'Reset feed cache', 'rss2cpt' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Clear the cached feed XML and any active import lock for the configured feed. Use this when you need the next import to re-fetch the feed from the source instead of using the cached response.', 'rss2cpt' ); ?></p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
|
||||
<input type="hidden" name="action" value="<?php echo esc_attr( self::RESET_CACHE_ACTION ); ?>">
|
||||
<?php wp_nonce_field( self::RESET_NONCE_ACTION ); ?>
|
||||
<?php submit_button( __( 'Reset cache', 'rss2cpt' ), 'delete', 'submit', false ); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -310,6 +325,88 @@ final class Settings {
|
||||
$this->redirect_with_notice( 'success', $counts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a verified cache-reset request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_reset_cache(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die(
|
||||
esc_html__( 'You are not allowed to clear the podcast feed cache.', 'rss2cpt' ),
|
||||
esc_html__( 'Forbidden', 'rss2cpt' ),
|
||||
array( 'response' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
check_admin_referer( self::RESET_NONCE_ACTION );
|
||||
|
||||
$settings = Options::get();
|
||||
$feed_url = (string) $settings['feed_url'];
|
||||
$post_type = (string) $settings['post_type'];
|
||||
|
||||
$cleared = $this->clear_feed_cache( $feed_url, $post_type );
|
||||
|
||||
$this->redirect_with_notice( $cleared ? 'cache_cleared' : 'cache_clear_failed' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the SimplePie feed-cache rows and the importer's per-feed lock.
|
||||
*
|
||||
* WordPress's WP_Feed_Cache_Transient stores the cached feed under
|
||||
* transients named "feed_<md5(url)>" and "feed_mod_<md5(url)>". On disk,
|
||||
* transients surface in the options table as _transient_* and
|
||||
* _transient_timeout_* rows; on multisite the corresponding
|
||||
* _site_transient_* rows are used. We delete all four standard variants
|
||||
* plus the two site-transient variants so the next import always re-fetches.
|
||||
*
|
||||
* The importer's lock key is "rss2cpt_lock_<md5(feed_url + '|' + post_type)>".
|
||||
*
|
||||
* @param string $feed_url Configured feed URL.
|
||||
* @param string $post_type Configured destination post type.
|
||||
* @return bool True when at least one cache row was deleted.
|
||||
*/
|
||||
public function clear_feed_cache( string $feed_url, string $post_type ): bool {
|
||||
if ( '' === $feed_url ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url_hash = md5( $feed_url );
|
||||
$lock_hash = md5( $feed_url . '|' . $post_type );
|
||||
|
||||
$option_keys = array(
|
||||
'_transient_feed_' . $url_hash,
|
||||
'_transient_timeout_feed_' . $url_hash,
|
||||
'_transient_feed_mod_' . $url_hash,
|
||||
'_transient_timeout_feed_mod_' . $url_hash,
|
||||
'rss2cpt_lock_' . $lock_hash,
|
||||
);
|
||||
|
||||
$cleared = false;
|
||||
foreach ( $option_keys as $key ) {
|
||||
if ( delete_option( $key ) ) {
|
||||
$cleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Multisite: feed cache may live in the site transients table.
|
||||
foreach ( array( 'feed_' . $url_hash, 'feed_mod_' . $url_hash ) as $transient ) {
|
||||
if ( delete_site_transient( $transient ) ) {
|
||||
$cleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after the plugin clears its feed cache and import lock.
|
||||
*
|
||||
* @param string $feed_url Feed URL whose cache was cleared.
|
||||
* @param string $post_type Destination post type.
|
||||
*/
|
||||
do_action( 'rss2cpt_cache_cleared', $feed_url, $post_type );
|
||||
|
||||
return $cleared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a manual-import result notice.
|
||||
*
|
||||
@@ -351,6 +448,12 @@ final class Settings {
|
||||
$failed,
|
||||
$warnings
|
||||
);
|
||||
} elseif ( 'cache_cleared' === $notice ) {
|
||||
$class = 'notice notice-success';
|
||||
$message = __( 'Feed cache cleared. The next import will re-fetch the feed from the source.', 'rss2cpt' );
|
||||
} elseif ( 'cache_clear_failed' === $notice ) {
|
||||
$class = 'notice notice-warning';
|
||||
$message = __( 'Feed cache could not be cleared. Make sure a feed URL is configured.', 'rss2cpt' );
|
||||
}
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once dirname( __DIR__ ) . '/src/Scheduler.php';
|
||||
require_once dirname( __DIR__ ) . '/src/Admin/Settings.php';
|
||||
|
||||
/**
|
||||
* Verifies project-level invariants without loading WordPress.
|
||||
*/
|
||||
@@ -133,4 +136,43 @@ final class SmokeTest extends TestCase {
|
||||
'fingerprint' => str_repeat( 'b', 64 ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that clear_feed_cache deletes the SimplePie feed-cache option
|
||||
* rows, the importer's per-feed lock, and the multisite site-transient
|
||||
* variants derived from the configured feed URL.
|
||||
*/
|
||||
public function test_clear_feed_cache_removes_lock_and_simplepie_transient_keys(): void {
|
||||
rss2cpt_test_reset_delete_logs();
|
||||
|
||||
$importer = new RSS2CPT\Import\Importer();
|
||||
$settings = new RSS2CPT\Admin\Settings( $importer, new RSS2CPT\Scheduler( $importer ) );
|
||||
$cleared = $settings->clear_feed_cache( 'https://example.test/feed.xml', 'podcast_episode' );
|
||||
|
||||
$this->assertTrue( $cleared );
|
||||
$this->assertContains( '_transient_feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertContains( '_transient_timeout_feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertContains( '_transient_feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertContains( '_transient_timeout_feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertContains( 'rss2cpt_lock_' . md5( 'https://example.test/feed.xml|podcast_episode' ), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertContains( 'feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_site_transients'] );
|
||||
$this->assertContains( 'feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_site_transients'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that clear_feed_cache refuses to do anything destructive when
|
||||
* the feed URL is empty (so an unconfigured install cannot wipe someone
|
||||
* else's transient rows by accident).
|
||||
*/
|
||||
public function test_clear_feed_cache_is_noop_when_feed_url_empty(): void {
|
||||
rss2cpt_test_reset_delete_logs();
|
||||
|
||||
$importer = new RSS2CPT\Import\Importer();
|
||||
$settings = new RSS2CPT\Admin\Settings( $importer, new RSS2CPT\Scheduler( $importer ) );
|
||||
$cleared = $settings->clear_feed_cache( '', 'podcast_episode' );
|
||||
|
||||
$this->assertFalse( $cleared );
|
||||
$this->assertSame( array(), $GLOBALS['rss2cpt_test_deleted_options'] );
|
||||
$this->assertSame( array(), $GLOBALS['rss2cpt_test_deleted_site_transients'] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +55,48 @@ function get_gmt_from_date( $date_string ) {
|
||||
return gmdate( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory option store for isolated tests.
|
||||
*
|
||||
* @param string $name Option name.
|
||||
* @return bool Always true; the test asserts via $GLOBALS['rss2cpt_test_deleted_options'].
|
||||
*/
|
||||
function delete_option( $name ) {
|
||||
$GLOBALS['rss2cpt_test_deleted_options'][] = $name;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory site-transient store for isolated tests.
|
||||
*
|
||||
* @param string $name Transient name.
|
||||
* @return bool Always true; the test asserts via $GLOBALS['rss2cpt_test_deleted_site_transients'].
|
||||
*/
|
||||
function delete_site_transient( $name ) {
|
||||
$GLOBALS['rss2cpt_test_deleted_site_transients'][] = $name;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset recorded delete_* call logs between tests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rss2cpt_test_reset_delete_logs() {
|
||||
$GLOBALS['rss2cpt_test_deleted_options'] = array();
|
||||
$GLOBALS['rss2cpt_test_deleted_site_transients'] = array();
|
||||
}
|
||||
rss2cpt_test_reset_delete_logs();
|
||||
|
||||
/**
|
||||
* Minimal do_action stub for isolated tests.
|
||||
*
|
||||
* @param string $hook Hook name.
|
||||
* @param mixed ...$args Hook arguments.
|
||||
*/
|
||||
function do_action( $hook, ...$args ) {
|
||||
unset( $hook, $args );
|
||||
}
|
||||
|
||||
require_once dirname( __DIR__ ) . '/src/Options.php';
|
||||
require_once dirname( __DIR__ ) . '/src/Import/Importer.php';
|
||||
|
||||
Reference in New Issue
Block a user