Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Skip404
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 9
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 is_active
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 get_value
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_update_htaccess
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_add_rules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 add_rules
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 remove_rules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_to_runtime
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 bootstrap_register
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Skip404
4 *
5 * Manages registration/unregistration of a .htaccess fragment that prevents
6 * WordPress 404 handling for static-file requests when the path looks like
7 * a static asset but the file/dir isn't present. Uses the centralized
8 * HtaccessApi fragment registry to ensure safe, debounced writes.
9 *
10 * @package NewfoldLabs\WP\Module\Performance\Skip404
11 * @since 1.0.0
12 */
13
14namespace NewfoldLabs\WP\Module\Performance\Skip404;
15
16use NewfoldLabs\WP\ModuleLoader\Container;
17use NewfoldLabs\WP\Module\Performance\OptionListener;
18use NewfoldLabs\WP\Module\Htaccess\Api as HtaccessApi;
19use NewfoldLabs\WP\Module\Performance\Skip404\Fragments\Skip404Fragment;
20
21/**
22 * Handles Skip 404 functionality.
23 *
24 * Registers/unregisters a fragment that short-circuits requests for typical
25 * static extensions (css/js/images, etc.) so Apache stops rewrite processing
26 * early instead of punting them into WP's 404 handler.
27 *
28 * @since 1.0.0
29 */
30class Skip404 {
31
32    /**
33     * Dependency injection container.
34     *
35     * @var Container
36     */
37    protected $container;
38
39    /**
40     * Option name for skip 404 setting.
41     *
42     * @var string
43     */
44    const OPTION_NAME = 'newfold_skip_404_handling';
45
46    /**
47     * Human-friendly marker text printed in BEGIN/END comments.
48     *
49     * @var string
50     */
51    const MARKER = 'Newfold Skip 404 Handling for Static Files';
52
53    /**
54     * Globally-unique fragment identifier used by the registry.
55     *
56     * @var string
57     */
58    const FRAGMENT_ID = 'nfd.skip404.static';
59
60    /**
61     * Constructor.
62     *
63     * @since 1.0.0
64     *
65     * @param Container $container The dependency injection container.
66     */
67    public function __construct( Container $container ) {
68        $this->container = $container;
69
70        new OptionListener( self::OPTION_NAME, array( __CLASS__, 'maybe_add_rules' ) );
71
72        // Bootstrap-register into the in-memory registry (no write) in maintenance contexts.
73        // Admin (runs for real wp-admin screens)
74        add_action( 'admin_init', array( __CLASS__, 'bootstrap_register' ), 20 );
75
76        // REST (runs when REST is bootstrapped; constants definitely set)
77        add_action( 'rest_api_init', array( __CLASS__, 'bootstrap_register' ), 20 );
78
79        // AJAX (runs on admin-ajax.php requests)
80        add_action(
81            'init',
82            function () {
83                if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
84                    \NewfoldLabs\WP\Module\Performance\Skip404\Skip404::bootstrap_register();
85                }
86            },
87            5
88        );
89
90        // CRON/CLI (admin_init doesn’t fire there)
91        add_action(
92            'init',
93            function () {
94                if ( ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() )
95                    || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
96                        \NewfoldLabs\WP\Module\Performance\Skip404\Skip404::bootstrap_register();
97                }
98            },
99            5
100        );
101
102        add_filter( 'newfold_update_htaccess', array( $this, 'on_update_htaccess' ) );
103        add_filter( 'newfold-runtime', array( $this, 'add_to_runtime' ), 100 );
104    }
105
106    /**
107     * Detect if the feature needs to be performed or not.
108     *
109     * @since 1.0.0
110     *
111     * @param Container $container Dependency injection container.
112     * @return bool
113     */
114    public static function is_active( Container $container ): bool {
115        return (bool) $container->has( 'isApache' ) && $container->get( 'isApache' );
116    }
117
118    /**
119     * Get value for SKIP404 option.
120     *
121     * @since 1.0.0
122     *
123     * @return bool
124     */
125    public static function get_value(): bool {
126        return (bool) get_option( self::OPTION_NAME, true );
127    }
128
129    /**
130     * When updating .htaccess, also update our rules as appropriate.
131     *
132     * Also cleans up an older EPC option if set.
133     *
134     * @since 1.0.0
135     * @return void
136     */
137    public function on_update_htaccess(): void {
138        self::maybe_add_rules( self::get_value() );
139
140        // Remove the old option from EPC, if it exists.
141        if ( $this->container->get( 'hasMustUsePlugin' ) && absint( get_option( 'epc_skip_404_handling', 0 ) ) ) {
142            update_option( 'epc_skip_404_handling', 0 );
143            delete_option( 'epc_skip_404_handling' );
144        }
145    }
146
147    /**
148     * Conditionally add or remove .htaccess rules based on option value.
149     *
150     * @since 1.0.0
151     *
152     * @param bool|null $should_skip_404_handling If we should enable Skip 404.
153     * @return void
154     */
155    public static function maybe_add_rules( $should_skip_404_handling ): void {
156        (bool) $should_skip_404_handling ? self::add_rules() : self::remove_rules();
157    }
158
159    /**
160     * Register (or replace) our fragment with the current settings.
161     *
162     * @since 1.0.0
163     * @return void
164     */
165    public static function add_rules(): void {
166        HtaccessApi::register(
167            new Skip404Fragment(
168                self::FRAGMENT_ID,
169                self::MARKER
170            ),
171            true // queue apply to coalesce writes
172        );
173    }
174
175    /**
176     * Unregister our fragment.
177     *
178     * @since 1.0.0
179     * @return void
180     */
181    public static function remove_rules(): void {
182        HtaccessApi::unregister( self::FRAGMENT_ID );
183    }
184
185    /**
186     * Add to Newfold SDK runtime.
187     *
188     * @since 1.0.0
189     *
190     * @param array $sdk SDK data.
191     * @return array SDK data.
192     */
193    public function add_to_runtime( $sdk ): array {
194        $values = array(
195            'is_active' => $this->get_value(),
196        );
197
198        return array_merge( $sdk, array( 'skip404' => $values ) );
199    }
200
201    /**
202     * Populate the registry so reconciliation/apply can “see” this fragment
203     * in admin, cron, CLI, REST and AJAX requests. No writes are queued.
204     *
205     * @since 1.0.0
206     * @return void
207     */
208    public static function bootstrap_register(): void {
209        // Respect the feature toggle; if disabled, don't register.
210        if ( false === self::get_value() ) {
211            return;
212        }
213
214        // Register into the in-memory registry ONLY (no apply/write).
215        HtaccessApi::register(
216            new Skip404Fragment( self::FRAGMENT_ID, self::MARKER ),
217            false
218        );
219    }
220}