Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PerformanceLifecycleHooks
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 12
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 plugin_hooks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 hooks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 on_activation
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 delete_plugin_list_option_cache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clear_plugin_cache_on_shutdown
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purge_object_cache_on_shutdown
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 on_deactivation
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 on_cache_level_change
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 get_response_header_manager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nfd_force_disable_epc_options
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 nfd_sync_epc_from_brand
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * PerformanceLifecycleHooks
4 *
5 * Unified lifecycle wiring for Performance features:
6 * - Cache headers (File + Browser types, ResponseHeaderManager)
7 * - Skip404 rules (.htaccess fragment via HtaccessApi)
8 *
9 * @package NewfoldLabs\WP\Module\Performance
10 * @since 1.0.0
11 */
12
13namespace NewfoldLabs\WP\Module\Performance;
14
15use NewfoldLabs\WP\Module\Performance\Cache\CacheManager;
16use NewfoldLabs\WP\Module\Performance\Cache\ResponseHeaderManager;
17use NewfoldLabs\WP\ModuleLoader\Container;
18use NewfoldLabs\WP\Module\Performance\OptionListener;
19use NewfoldLabs\WP\Module\Performance\Cache\Types\Browser;
20use NewfoldLabs\WP\Module\Performance\Cache\Types\File;
21use NewfoldLabs\WP\Module\Performance\Cache\Types\ObjectCache;
22use NewfoldLabs\WP\Module\Performance\Images\ImageRewriteHandler;
23use NewfoldLabs\WP\Module\Performance\Skip404\Skip404;
24
25use function NewfoldLabs\WP\Module\Performance\get_cache_level;
26use function NewfoldLabs\WP\ModuleLoader\container;
27
28/**
29 * Class PerformanceLifecycleHooks
30 *
31 * Combines lifecycle hooks for Cache and Skip404 so you can bootstrap once.
32 *
33 * @since 1.0.0
34 */
35class PerformanceLifecycleHooks {
36
37    /**
38     * Dependency injection container.
39     *
40     * @var Container
41     */
42    protected $container;
43
44    /**
45     * Whether plugin_hooks has already run (avoids double-registering).
46     *
47     * @var bool
48     */
49    protected $plugin_hooks_done = false;
50
51    /**
52     * Constructor.
53     */
54    public function __construct() {
55        if ( function_exists( 'add_action' ) ) {
56            add_action( 'newfold_container_set', array( $this, 'plugin_hooks' ) );
57            add_action( 'plugins_loaded', array( $this, 'hooks' ) );
58            // If Redis config was removed from wp-config but our drop-in is still present, remove it so the site does not break.
59            add_action( 'plugins_loaded', array( ObjectCache::class, 'maybe_remove_dropin_if_unavailable' ), 1 );
60            // Reconcile any non-ours object-cache.php with the user's preference (replace or leave).
61            add_action( 'plugins_loaded', array( ObjectCache::class, 'reconcile_non_ours_dropin' ), 2 );
62
63            // Do not call container() here. The ModuleLoader's container() creates and locks in
64            // an empty container if called before the host plugin calls setContainer(), which
65            // causes "No entry was found for 'plugin'" when Features etc. run.
66
67            // Keep Cache level header in sync with option changes.
68            new OptionListener( CacheManager::OPTION_CACHE_LEVEL, array( $this, 'on_cache_level_change' ) );
69        }
70    }
71
72    /**
73     * Hooks for plugin activation/deactivation.
74     *
75     * @param Container $container From the plugin.
76     * @return void
77     */
78    public function plugin_hooks( Container $container ) {
79        if ( $this->plugin_hooks_done ) {
80            return;
81        }
82        $this->plugin_hooks_done = true;
83        $this->container         = $container;
84
85        register_activation_hook(
86            $container->plugin()->file,
87            array( $this, 'on_activation' )
88        );
89
90        register_deactivation_hook(
91            $container->plugin()->file,
92            array( $this, 'on_deactivation' )
93        );
94    }
95
96    /**
97     * Add feature enable/disable hooks.
98     *
99     * @return void
100     */
101    public function hooks() {
102        add_action(
103            'newfold/features/action/onEnable:performance',
104            array( $this, 'on_activation' )
105        );
106
107        add_action(
108            'newfold/features/action/onDisable:performance',
109            array( $this, 'on_deactivation' )
110        );
111    }
112
113    /**
114     * Activation/Enable: apply Cache + Skip404.
115     *
116     * @since 1.0.0
117     * @return void
118     */
119    public function on_activation() {
120        // Clear plugin list from object cache on shutdown so the next request reads active_plugins from DB (not Redis).
121        // Only clears options group — avoids full flush which would destroy session/auth data and log the user out.
122        add_action( 'shutdown', array( $this, 'clear_plugin_cache_on_shutdown' ), PHP_INT_MAX );
123        // Cache feature bits.
124        File::on_activation();
125        Browser::on_activation();
126
127        // Restore object-cache drop-in if Redis constants exist and user had it enabled before deactivation.
128        ObjectCache::maybe_restore_on_activation();
129
130        // Image rewrite rules.
131        ImageRewriteHandler::on_activation();
132
133        // Skip404 rules based on current option value.
134        Skip404::maybe_add_rules( Skip404::get_value() );
135
136        // Ensure EPC is off and removes its rules
137        $this->nfd_force_disable_epc_options();
138    }
139
140    /**
141     * Delete object-cache keys for active_plugins and alloptions so the next request reads from DB.
142     * Prevents stale plugin list when object cache (e.g. Redis) is enabled, which can make
143     * activation/deactivation appear to fail the first time.
144     *
145     * @return void
146     */
147    protected function delete_plugin_list_option_cache() {
148        ObjectCache::clear_options_object_cache();
149    }
150
151    /**
152     * Clear only the plugin list from object cache on shutdown after activation.
153     * Ensures the next request reads active_plugins from DB without destroying session/auth data.
154     *
155     * @return void
156     */
157    public function clear_plugin_cache_on_shutdown() {
158        $this->delete_plugin_list_option_cache();
159    }
160
161    /**
162     * Purge object cache (options + full flush + runtime) on shutdown after deactivation.
163     * Ensures the next request reads active_plugins from DB even if something re-cached after our hooks.
164     *
165     * @return void
166     */
167    public function purge_object_cache_on_shutdown() {
168        $this->delete_plugin_list_option_cache();
169        ObjectCache::flush_object_cache();
170        if ( function_exists( 'wp_cache_flush' ) ) {
171            wp_cache_flush();
172        }
173        if ( function_exists( 'wp_cache_flush_runtime' ) ) {
174            wp_cache_flush_runtime();
175        }
176    }
177
178    /**
179     * Deactivation/Disable: remove Cache + Skip404.
180     *
181     * @since 1.0.0
182     * @return void
183     */
184    public function on_deactivation() {
185        // Purge object cache on shutdown so the next request reads active_plugins from DB (not Redis).
186        add_action( 'shutdown', array( $this, 'purge_object_cache_on_shutdown' ), PHP_INT_MAX );
187
188        // Cache feature bits.
189        File::on_deactivation();
190        Browser::on_deactivation();
191
192        // Remove our object-cache drop-in if present (only deletes if it's our file).
193        ObjectCache::on_deactivation();
194
195        // Remove image rewrite rules.
196        ImageRewriteHandler::on_deactivation();
197
198        // Remove all headers written by ResponseHeaderManager.
199        $response_header_manager = $this->get_response_header_manager();
200        if ( $response_header_manager ) {
201            $response_header_manager->remove_all_headers();
202        }
203
204        // Remove Skip404 rules.
205        Skip404::remove_rules();
206
207        // Hand settings back to EPC to match the brand plugin's current values
208        $this->nfd_sync_epc_from_brand();
209    }
210
211    /**
212     * On cache level change, update the response header and clean up legacy EPC option.
213     *
214     * @return void
215     */
216    public function on_cache_level_change() {
217
218        // Remove the old option from EPC, if it exists.
219        if ( $this->container && $this->container->get( 'hasMustUsePlugin' ) && absint( get_option( 'endurance_cache_level', 0 ) ) ) {
220            update_option( 'endurance_cache_level', 0 );
221            delete_option( 'endurance_cache_level' );
222        }
223    }
224
225    /**
226     * Helper to fetch ResponseHeaderManager from the container (if available).
227     *
228     * @return \NewfoldLabs\WP\Module\Performance\ResponseHeaderManager|null
229     */
230    protected function get_response_header_manager() {
231        return new ResponseHeaderManager();
232    }
233
234    /**
235     * Force Endurance Page Cache off by clamping its options to 0.
236     * Triggers EPC to remove its own rules, then tidies the options.
237     */
238    private function nfd_force_disable_epc_options(): void {
239        $changed = false;
240
241        // Clamp EPC options to 0 so its own code tears down rules.
242        if ( (int) get_option( 'endurance_cache_level', 0 ) !== 0 ) {
243            update_option( 'endurance_cache_level', 0 );
244            $changed = true;
245        }
246        if ( (int) get_option( 'epc_skip_404_handling', 0 ) !== 0 ) {
247            update_option( 'epc_skip_404_handling', 0 );
248            $changed = true;
249        }
250
251        // If anything changed, write .htaccess once and tidy options.
252        if ( $changed ) {
253            if ( ! function_exists( 'save_mod_rewrite_rules' ) ) {
254                require_once ABSPATH . 'wp-admin/includes/misc.php';
255            }
256            // Causes WP to regenerate rules; EPC listeners (if loaded) have just been triggered by the updates above.
257            save_mod_rewrite_rules();
258
259            // Optional cleanup so these don't linger in the DB.
260            delete_option( 'endurance_cache_level' );
261            delete_option( 'epc_skip_404_handling' );
262        }
263    }
264
265    /**
266     * When the brand plugin is deactivated, mirror its current settings into EPC.
267     * - EPC cache level (endurance_cache_level) is set to the current brand cache level (0–3).
268     * - EPC skip404 (epc_skip_404_handling) is set to the current brand skip404 value (0/1).
269     */
270    private function nfd_sync_epc_from_brand(): void {
271        // Clamp to EPC's range 0–3
272        $brand_level = (int) max( 0, min( 3, get_cache_level() ) );
273
274        // Brand Skip404: true/false -> EPC 1/0
275        $brand_skip404 = Skip404::get_value() ? 1 : 0;
276
277        // Write EPC options to reflect the brand plugin's current state
278        update_option( 'endurance_cache_level', $brand_level );
279        update_option( 'epc_skip_404_handling', $brand_skip404 );
280
281        // Ask WP to regenerate .htaccess so EPC can add/remove its own rules accordingly
282        if ( ! function_exists( 'save_mod_rewrite_rules' ) ) {
283            require_once ABSPATH . 'wp-admin/includes/misc.php';
284        }
285        save_mod_rewrite_rules();
286    }
287}