Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PerformanceLifecycleHooks
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 11
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
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
 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
61            // Do not call container() here. The ModuleLoader's container() creates and locks in
62            // an empty container if called before the host plugin calls setContainer(), which
63            // causes "No entry was found for 'plugin'" when Features etc. run.
64
65            // Keep Cache level header in sync with option changes.
66            new OptionListener( CacheManager::OPTION_CACHE_LEVEL, array( $this, 'on_cache_level_change' ) );
67        }
68    }
69
70    /**
71     * Hooks for plugin activation/deactivation.
72     *
73     * @param Container $container From the plugin.
74     * @return void
75     */
76    public function plugin_hooks( Container $container ) {
77        if ( $this->plugin_hooks_done ) {
78            return;
79        }
80        $this->plugin_hooks_done = true;
81        $this->container         = $container;
82
83        register_activation_hook(
84            $container->plugin()->file,
85            array( $this, 'on_activation' )
86        );
87
88        register_deactivation_hook(
89            $container->plugin()->file,
90            array( $this, 'on_deactivation' )
91        );
92    }
93
94    /**
95     * Add feature enable/disable hooks.
96     *
97     * @return void
98     */
99    public function hooks() {
100        add_action(
101            'newfold/features/action/onEnable:performance',
102            array( $this, 'on_activation' )
103        );
104
105        add_action(
106            'newfold/features/action/onDisable:performance',
107            array( $this, 'on_deactivation' )
108        );
109    }
110
111    /**
112     * Activation/Enable: apply Cache + Skip404.
113     *
114     * @since 1.0.0
115     * @return void
116     */
117    public function on_activation() {
118        // Purge object cache on shutdown so the next request reads active_plugins from DB (not Redis).
119        add_action( 'shutdown', array( $this, 'purge_object_cache_on_shutdown' ), PHP_INT_MAX );
120        // Cache feature bits.
121        File::on_activation();
122        Browser::on_activation();
123
124        // Restore object-cache drop-in if Redis constants exist and user had it enabled before deactivation.
125        ObjectCache::maybe_restore_on_activation();
126
127        // Image rewrite rules.
128        ImageRewriteHandler::on_activation();
129
130        // Skip404 rules based on current option value.
131        Skip404::maybe_add_rules( Skip404::get_value() );
132
133        // Ensure EPC is off and removes its rules
134        $this->nfd_force_disable_epc_options();
135    }
136
137    /**
138     * Delete object-cache keys for active_plugins and alloptions so the next request reads from DB.
139     * Prevents stale plugin list when object cache (e.g. Redis) is enabled, which can make
140     * activation/deactivation appear to fail the first time.
141     *
142     * @return void
143     */
144    protected function delete_plugin_list_option_cache() {
145        ObjectCache::clear_options_object_cache();
146    }
147
148    /**
149     * Purge object cache (options + full flush + runtime) on shutdown after activate/deactivate.
150     * Ensures the next request reads active_plugins from DB even if something re-cached after our hooks.
151     *
152     * @return void
153     */
154    public function purge_object_cache_on_shutdown() {
155        $this->delete_plugin_list_option_cache();
156        ObjectCache::flush_object_cache();
157        if ( function_exists( 'wp_cache_flush' ) ) {
158            wp_cache_flush();
159        }
160        if ( function_exists( 'wp_cache_flush_runtime' ) ) {
161            wp_cache_flush_runtime();
162        }
163    }
164
165    /**
166     * Deactivation/Disable: remove Cache + Skip404.
167     *
168     * @since 1.0.0
169     * @return void
170     */
171    public function on_deactivation() {
172        // Purge object cache on shutdown so the next request reads active_plugins from DB (not Redis).
173        add_action( 'shutdown', array( $this, 'purge_object_cache_on_shutdown' ), PHP_INT_MAX );
174
175        // Cache feature bits.
176        File::on_deactivation();
177        Browser::on_deactivation();
178
179        // Remove our object-cache drop-in if present (only deletes if it's our file).
180        ObjectCache::on_deactivation();
181
182        // Remove image rewrite rules.
183        ImageRewriteHandler::on_deactivation();
184
185        // Remove all headers written by ResponseHeaderManager.
186        $response_header_manager = $this->get_response_header_manager();
187        if ( $response_header_manager ) {
188            $response_header_manager->remove_all_headers();
189        }
190
191        // Remove Skip404 rules.
192        Skip404::remove_rules();
193
194        // Hand settings back to EPC to match the brand plugin's current values
195        $this->nfd_sync_epc_from_brand();
196    }
197
198    /**
199     * On cache level change, update the response header and clean up legacy EPC option.
200     *
201     * @return void
202     */
203    public function on_cache_level_change() {
204
205        // Remove the old option from EPC, if it exists.
206        if ( $this->container && $this->container->get( 'hasMustUsePlugin' ) && absint( get_option( 'endurance_cache_level', 0 ) ) ) {
207            update_option( 'endurance_cache_level', 0 );
208            delete_option( 'endurance_cache_level' );
209        }
210    }
211
212    /**
213     * Helper to fetch ResponseHeaderManager from the container (if available).
214     *
215     * @return \NewfoldLabs\WP\Module\Performance\ResponseHeaderManager|null
216     */
217    protected function get_response_header_manager() {
218        return new ResponseHeaderManager();
219    }
220
221    /**
222     * Force Endurance Page Cache off by clamping its options to 0.
223     * Triggers EPC to remove its own rules, then tidies the options.
224     */
225    private function nfd_force_disable_epc_options(): void {
226        $changed = false;
227
228        // Clamp EPC options to 0 so its own code tears down rules.
229        if ( (int) get_option( 'endurance_cache_level', 0 ) !== 0 ) {
230            update_option( 'endurance_cache_level', 0 );
231            $changed = true;
232        }
233        if ( (int) get_option( 'epc_skip_404_handling', 0 ) !== 0 ) {
234            update_option( 'epc_skip_404_handling', 0 );
235            $changed = true;
236        }
237
238        // If anything changed, write .htaccess once and tidy options.
239        if ( $changed ) {
240            if ( ! function_exists( 'save_mod_rewrite_rules' ) ) {
241                require_once ABSPATH . 'wp-admin/includes/misc.php';
242            }
243            // Causes WP to regenerate rules; EPC listeners (if loaded) have just been triggered by the updates above.
244            save_mod_rewrite_rules();
245
246            // Optional cleanup so these don't linger in the DB.
247            delete_option( 'endurance_cache_level' );
248            delete_option( 'epc_skip_404_handling' );
249        }
250    }
251
252    /**
253     * When the brand plugin is deactivated, mirror its current settings into EPC.
254     * - EPC cache level (endurance_cache_level) is set to the current brand cache level (0–3).
255     * - EPC skip404 (epc_skip_404_handling) is set to the current brand skip404 value (0/1).
256     */
257    private function nfd_sync_epc_from_brand(): void {
258        // Clamp to EPC's range 0–3
259        $brand_level = (int) max( 0, min( 3, get_cache_level() ) );
260
261        // Brand Skip404: true/false -> EPC 1/0
262        $brand_skip404 = Skip404::get_value() ? 1 : 0;
263
264        // Write EPC options to reflect the brand plugin's current state
265        update_option( 'endurance_cache_level', $brand_level );
266        update_option( 'epc_skip_404_handling', $brand_skip404 );
267
268        // Ask WP to regenerate .htaccess so EPC can add/remove its own rules accordingly
269        if ( ! function_exists( 'save_mod_rewrite_rules' ) ) {
270            require_once ABSPATH . 'wp-admin/includes/misc.php';
271        }
272        save_mod_rewrite_rules();
273    }
274}