Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageRewriteHandler
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 8
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_missing_image_rule
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 add_existing_image_rule
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 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 on_activation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_deactivation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_image_setting_change
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 apply_rules_from_option
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * ImageRewriteHandler
4 *
5 * Manages registration/unregistration of .htaccess fragments that redirect image
6 * requests to their optimized WebP counterparts. Uses the centralized
7 * HtaccessApi fragment registry to ensure safe, debounced writes.
8 *
9 * @package NewfoldLabs\WP\Module\Performance\Images
10 * @since 1.0.0
11 */
12
13namespace NewfoldLabs\WP\Module\Performance\Images;
14
15use NewfoldLabs\WP\Module\Htaccess\Api as HtaccessApi;
16use NewfoldLabs\WP\Module\Performance\Images\Fragments\MissingImageRedirectFragment;
17use NewfoldLabs\WP\Module\Performance\Images\Fragments\ExistingImageRedirectFragment;
18
19/**
20 * Handles the management of .htaccess rules for optimized image redirects.
21 *
22 * This class listens for image optimization option changes and registers or
23 * unregisters the appropriate fragments that:
24 *  - Prefer WebP when the original exists and a .webp variant is available.
25 *  - Serve a .webp file if the original asset is missing but the .webp exists.
26 *
27 * @since 1.0.0
28 */
29class ImageRewriteHandler {
30
31    /**
32     * Human-friendly marker for the "missing image → .webp" rule block.
33     *
34     * Printed in the BEGIN/END comments inside the .htaccess file.
35     *
36     * @var string
37     */
38    const MISSING_IMAGE_MARKER = 'Newfold WebP Missing Image Redirect';
39
40    /**
41     * Human-friendly marker for the "existing image → prefer .webp" rule block.
42     *
43     * Printed in the BEGIN/END comments inside the .htaccess file.
44     *
45     * @var string
46     */
47    const EXISTING_IMAGE_MARKER = 'Newfold WebP Existing Image Redirect';
48
49    /**
50     * Globally-unique fragment ID for the "missing image → .webp" rules.
51     *
52     * @var string
53     */
54    const FRAGMENT_ID_MISSING = 'nfd.images.webp.missing';
55
56    /**
57     * Globally-unique fragment ID for the "existing image → prefer .webp" rules.
58     *
59     * @var string
60     */
61    const FRAGMENT_ID_EXISTING = 'nfd.images.webp.existing';
62
63    /**
64     * Constructor. Hooks settings listener.
65     *
66     * @since 1.0.0
67     */
68    public function __construct() {
69        // Listen to image optimization settings changes.
70        add_action( 'update_option_nfd_image_optimization', array( __CLASS__, 'on_image_setting_change' ), 10, 2 );
71    }
72
73    /**
74     * Register the "missing image → .webp" fragment.
75     *
76     * This fragment rewrites requests for non-existent original images to a .webp
77     * counterpart when it exists on disk.
78     *
79     * @since 1.0.0
80     *
81     * @return void
82     */
83    public static function add_missing_image_rule(): void {
84        HtaccessApi::register(
85            new MissingImageRedirectFragment(
86                self::FRAGMENT_ID_MISSING,
87                self::MISSING_IMAGE_MARKER
88            ),
89            true // queue apply to coalesce writes
90        );
91    }
92
93    /**
94     * Register the "existing image → prefer .webp" fragment.
95     *
96     * This fragment rewrites requests for existing original images to a .webp
97     * counterpart when it exists (prefers optimized variant).
98     *
99     * @since 1.0.0
100     *
101     * @return void
102     */
103    public static function add_existing_image_rule(): void {
104        HtaccessApi::register(
105            new ExistingImageRedirectFragment(
106                self::FRAGMENT_ID_EXISTING,
107                self::EXISTING_IMAGE_MARKER
108            ),
109            true // queue apply to coalesce writes
110        );
111    }
112
113    /**
114     * Unregister both image redirect fragments.
115     *
116     * @since 1.0.0
117     *
118     * @return void
119     */
120    public static function remove_rules(): void {
121        HtaccessApi::unregister( self::FRAGMENT_ID_MISSING );
122        HtaccessApi::unregister( self::FRAGMENT_ID_EXISTING );
123    }
124
125    /**
126     * Activation hook: ensure relevant fragments are registered.
127     *
128     * Note: This registers both fragments; runtime settings may later
129     * unregister one/both via on_image_setting_change().
130     *
131     * @since 1.0.0
132     *
133     * @return void
134     */
135    public static function on_activation(): void {
136        self::apply_rules_from_option();
137    }
138
139    /**
140     * Deactivation hook: remove all related fragments.
141     *
142     * @since 1.0.0
143     *
144     * @return void
145     */
146    public static function on_deactivation(): void {
147        self::remove_rules();
148    }
149
150    /**
151     * Handle changes to image optimization settings.
152     *
153     * Expects the `nfd_image_optimization` option to be an associative array:
154     * - enabled (bool): master switch for image optimization features.
155     * - auto_optimized_uploaded_images['auto_delete_original_image'] (bool):
156     *     If true, enable "missing image → .webp" rewrite.
157     * - prefer_optimized_image_when_exists (bool):
158     *     If true, enable "existing image → prefer .webp" rewrite.
159     *
160     * @since 1.0.0
161     *
162     * @param mixed $old_value Previous settings value (unused by this handler).
163     * @param mixed $new_value New settings value.
164     * @return void
165     */
166    public static function on_image_setting_change( $old_value, $new_value ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterface
167        // Remove rules if the entire feature is disabled.
168        if ( empty( $new_value['enabled'] ) ) {
169            self::remove_rules();
170            return;
171        }
172
173        // Missing-image redirect toggle (auto-delete original images).
174        if ( ! empty( $new_value['auto_optimized_uploaded_images']['auto_delete_original_image'] ) ) {
175            self::add_missing_image_rule();
176        } else {
177            HtaccessApi::unregister( self::FRAGMENT_ID_MISSING );
178        }
179
180        // Prefer-optimized-image toggle.
181        if ( ! empty( $new_value['prefer_optimized_image_when_exists'] ) ) {
182            self::add_existing_image_rule();
183        } else {
184            HtaccessApi::unregister( self::FRAGMENT_ID_EXISTING );
185        }
186    }
187
188    /**
189     * Apply rules based on the current `nfd_image_optimization` option value.
190     *
191     * Static helper intended for use on activation or boot to sync .htaccess
192     * fragments with the saved settings, performing the same logic as
193     * on_image_setting_change().
194     *
195     * @since 1.0.0
196     *
197     * @return void
198     */
199    public static function apply_rules_from_option(): void {
200        $current = get_option( 'nfd_image_optimization', array() );
201        self::on_image_setting_change( null, is_array( $current ) ? $current : array() );
202    }
203}