Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Performance
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 16
1980
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 configureContainer
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 hooks
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 remove_epc_settings
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 nfd_asr_default
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nfd_as_cleanup_batch_size
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 admin_bar_menu
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
30
 add_management_page
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 add_nfd_subnav
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 render_performance_app
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initialize_performance_app
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_performance_assets
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 initialize_lazy_loader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 add_dummy_performance_menu_link
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 handle_performance_redirect
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 old_performance_redirect
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace NewfoldLabs\WP\Module\Performance;
4
5use NewfoldLabs\WP\ModuleLoader\Container;
6use NewfoldLabs\WP\Module\Performance\Images\ImageManager;
7use NewfoldLabs\WP\Module\Performance\RestApi\RestApi;
8use NewfoldLabs\WP\Module\Performance\Data\Constants;
9use NewfoldLabs\WP\Module\Performance\Services\I18nService;
10use NewfoldLabs\WP\Module\Performance\LinkPrefetch\LinkPrefetch;
11use NewfoldLabs\WP\Module\Performance\Cache\Cache;
12use NewfoldLabs\WP\Module\Performance\Cache\ResponseHeaderManager;
13use NewfoldLabs\WP\Module\Performance\Cloudflare\CloudflareFeaturesManager;
14use NewfoldLabs\WP\Module\Performance\Fonts\FontManager;
15use NewfoldLabs\WP\Module\Performance\Skip404\Skip404;
16use NewfoldLabs\WP\Module\Performance\JetpackBoost\JetpackBoost;
17
18use function NewfoldLabs\WP\Module\Performance\get_cache_level;
19use function NewfoldLabs\WP\ModuleLoader\container;
20
21/**
22 * Main class for the performance module.
23 */
24class Performance {
25
26    /**
27     * URL parameter used to purge the entire cache.
28     *
29     * @var string
30     */
31    const PURGE_ALL = 'nfd_purge_all';
32
33    /**
34     * URL parameter used to purge the cache for a specific URL.
35     *
36     * @var string
37     */
38    const PURGE_URL = 'nfd_purge_url';
39
40    /**
41    * Slug used for the Performance module's admin page.
42    *
43    * @var string
44    */
45    const PAGE_SLUG = 'nfd-performance';
46
47    /**
48     * Dependency injection container.
49     *
50     * @var Container
51     */
52    protected $container;
53
54    /**
55     * Constructor.
56     *
57     * @param Container $container the container
58     */
59    public function __construct( Container $container ) {
60
61        $this->container = $container;
62        $this->configureContainer( $container );
63
64        $this->hooks();
65
66        new Cache( $container );
67        new Skip404( $container );
68        new PerformanceWPCLI();
69        new Constants( $container );
70        new CloudflareFeaturesManager( $container );
71        new ImageManager( $container );
72        new FontManager( $container );
73        new HealthChecks( $container );
74
75        new LinkPrefetch( $container );
76
77        new JetpackBoost( $container );
78
79        if ( Permissions::is_authorized_admin() || Permissions::rest_is_authorized_admin() ) {
80            new RestAPI();
81        }
82
83        add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 );
84        add_action( 'admin_menu', array( $this, 'add_management_page' ) );
85        add_action( 'load-tools_page_' . self::PAGE_SLUG, array( __CLASS__, 'initialize_performance_app' ) );
86        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'initialize_performance_app' ) );
87        add_filter( 'nfd_plugin_subnav', array( $this, 'add_nfd_subnav' ) );
88        add_action( 'admin_init', array( __CLASS__, 'handle_performance_redirect' ) );
89        add_action( 'admin_menu', array( __CLASS__, 'add_dummy_performance_menu_link' ) );
90
91        ! defined( 'NFD_PERFORMANCE_PLUGIN_LANGUAGES_DIR' ) && define( 'NFD_PERFORMANCE_PLUGIN_LANGUAGES_DIR', dirname( $container->plugin()->file ) . '/vendor/newfold-labs/wp-module-performance/languages' );
92        new I18nService( $container );
93    }
94
95    /**
96     * Constructor.
97     *
98     * @param Container $container the container.
99     */
100    public function configureContainer( Container $container ) {
101
102        $is_apache = false;
103
104        // Ensure $is_apache is properly set, with a fallback for WP-CLI environment
105        if ( NFD_WPCLI::is_executing_wp_cli() ) {
106            // Attempt to detect Apache based on the SERVER_SOFTWARE header
107            $is_apache = isset( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], 'apache' ) !== false;
108
109            // Check for the existence of an .htaccess file (commonly used in Apache environments)
110            if ( ! $is_apache && file_exists( ABSPATH . '.htaccess' ) ) {
111                $is_apache = true;
112            }
113        } else {
114            global $is_apache;
115        }
116
117        $container->set( 'isApache', $is_apache );
118
119        $container->set(
120            'responseHeaderManager',
121            $container->service(
122                function () {
123                    return new ResponseHeaderManager();
124                }
125            )
126        );
127    }
128
129    /**
130     * Add hooks.
131     */
132    public function hooks() {
133
134        add_action( 'admin_init', array( $this, 'remove_epc_settings' ), 99 );
135
136        /**
137         * On CLI requests, mod_rewrite is unavailable, so it fails to update
138         * the .htaccess file when save_mod_rewrite_rules() is called. This
139         * forces that to be true so updates from WP CLI work.
140         */
141        if ( defined( 'WP_CLI' ) && WP_CLI ) {
142            add_filter( 'got_rewrite', '__return_true' );
143        }
144
145        if ( isset( $_REQUEST['action'], $_REQUEST['plugin'] ) && 'activate' === $_REQUEST['action'] && $_REQUEST['plugin'] === $this->container->plugin()->basename ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
146            add_filter(
147                'mod_rewrite_rules',
148                function ( $content ) {
149                    add_action(
150                        'shutdown',
151                        function () {
152                            do_action( 'newfold_update_htaccess' );
153                        }
154                    );
155                    return $content;
156                }
157            );
158        }
159
160        add_filter( 'action_scheduler_retention_period', array( $this, 'nfd_asr_default' ) );
161        add_filter( 'action_scheduler_cleanup_batch_size', array( $this, 'nfd_as_cleanup_batch_size' ) );
162        add_filter( 'newfold_performance_images_initialize_lazy_loader', array( $this, 'initialize_lazy_loader' ), 10, 1 );
163    }
164
165    /**
166     * Remove EPC Settings if needed
167     *
168     * @return void
169     */
170    public function remove_epc_settings() {
171        global $wp_settings_fields, $wp_settings_sections;
172        //phpcs:ignore
173        // Remove the setting from EPC if it exists - TODO: Remove when no longer using EPC
174        if ( $this->container->get( 'hasMustUsePlugin' ) ) {
175            unset( $wp_settings_fields['general']['epc_settings_section'] );
176            unset( $wp_settings_sections['general']['epc_settings_section'] );
177            unregister_setting( 'general', 'endurance_cache_level' );
178            unregister_setting( 'general', 'epc_skip_404_handling' );
179        }
180    }
181
182    /**
183     * Update the default action scheduler retention period to 5 days instead of 30.
184     * The actions scheduler table tends to grow to gigantic sizes and this should help.
185     *
186     * @hooked action_scheduler_retention_period
187     * @see ActionScheduler_QueueCleaner::delete_old_actions()
188     *
189     * @return int New retention period in seconds.
190     */
191    public function nfd_asr_default() {
192        return 5 * constant( 'DAY_IN_SECONDS' );
193    }
194
195    /**
196     * Increase the batch size for the cleanup process from default of 20 to 1000.
197     *
198     * @hooked action_scheduler_cleanup_batch_size
199     * @see ActionScheduler_QueueCleaner::get_batch_size()
200     *
201     * @param int $batch_size Existing batch size; default is 20.
202     *
203     * @return int 1000 when running the cleanup process, otherwise the existing batch size.
204     */
205    public function nfd_as_cleanup_batch_size( $batch_size ) {
206        /**
207         * Apply only to {@see ActionScheduler_QueueCleaner::delete_old_actions()} and not to
208         * {@see ActionScheduler_QueueCleaner::reset_timeouts()} or
209         * {@see ActionScheduler_QueueCleaner::mark_failures()} batch sizes.
210         */
211        if ( ! did_filter( 'action_scheduler_retention_period' ) ) {
212            return $batch_size;
213        }
214
215        return 1000;
216    }
217
218    /**
219     * Add options to the WordPress admin bar.
220     *
221     * @param \WP_Admin_Bar $wp_admin_bar the admin bar.
222     */
223    public function admin_bar_menu( \WP_Admin_Bar $wp_admin_bar ) {
224
225        // If the EPC MU plugin exists, remove its cache clearing options.
226        if ( $this->container->get( 'hasMustUsePlugin' ) ) {
227            $wp_admin_bar->remove_node( 'epc_purge_menu' );
228        }
229
230        if ( current_user_can( 'manage_options' ) ) {
231            $wp_admin_bar->add_node(
232                array(
233                    'id'    => 'nfd_purge_menu',
234                    'title' => __( 'Caching', 'wp-module-performance' ),
235                )
236            );
237
238            $cache_level = get_cache_level();
239            if ( $cache_level > 0 ) {
240                $wp_admin_bar->add_node(
241                    array(
242                        'id'     => 'nfd_purge_menu-purge_all',
243                        'title'  => __( 'Purge All', 'wp-module-performance' ),
244                        'parent' => 'nfd_purge_menu',
245                        'href'   => apply_filters( 'nfd_build_url', add_query_arg( array( self::PURGE_ALL => true ) ) ),
246                    )
247                );
248
249                if ( ! is_admin() ) {
250                    $wp_admin_bar->add_node(
251                        array(
252                            'id'     => 'nfd_purge_menu-purge_single',
253                            'title'  => __( 'Purge This Page', 'wp-module-performance' ),
254                            'parent' => 'nfd_purge_menu',
255                            'href'   => apply_filters( 'nfd_build_url', add_query_arg( array( self::PURGE_URL => true ) ) ),
256                        )
257                    );
258                }
259            }
260
261            $wp_admin_bar->add_node(
262                array(
263                    'id'     => 'nfd_purge_menu-cache_settings',
264                    'title'  => __( 'Cache Settings', 'wp-module-performance' ),
265                    'parent' => 'nfd_purge_menu',
266                    'href'   => apply_filters( 'nfd_build_url', admin_url( 'tools.php?page=' . self::PAGE_SLUG ) ),
267                )
268            );
269        }
270    }
271
272    /**
273     * Adds the Performance module to the WordPress Tools menu.
274     *
275     * @return void
276     */
277    public function add_management_page() {
278
279        add_management_page(
280            __( 'Performance', 'wp-module-performance' ),
281            __( 'Performance', 'wp-module-performance' ),
282            'manage_options',
283            container()->plugin()->id . '#/settings/performance',
284            array( __CLASS__, 'render_performance_app' )
285        );
286    }
287
288    /**
289     * Add to the Newfold subnav.
290     *
291     * @param array $subnav The nav array.
292     * @return array The filtered nav array
293     */
294    public function add_nfd_subnav( $subnav ) {
295        $performance = array(
296            'route'    => container()->plugin()->id . '#/settings/performance',
297            'title'    => __( 'Performance', 'wp-module-performance' ),
298            'priority' => 61,
299        );
300        array_push( $subnav, $performance );
301        return $subnav;
302    }
303
304    /**
305     * Outputs the HTML container for the Performance module's React application.
306     *
307     * @return void
308     */
309    public static function render_performance_app() {
310        echo PHP_EOL;
311        echo '<!-- NFD:PERFORMANCE -->';
312        echo PHP_EOL;
313        echo '<div id="' . esc_attr( self::PAGE_SLUG ) . '" class="' . esc_attr( self::PAGE_SLUG ) . '-container"></div>';
314        echo PHP_EOL;
315        echo '<!-- /NFD:PERFORMANCE -->';
316        echo PHP_EOL;
317    }
318
319    /**
320     * Initializes the Performance module by registering and enqueuing its assets.
321     *
322     * @return void
323     */
324    public static function initialize_performance_app() {
325        self::register_performance_assets();
326    }
327
328    /**
329     * Registers and enqueues the JavaScript and CSS assets for the Performance module.
330     *
331     * @return void
332     */
333    public static function register_performance_assets() {
334        $build_dir  = NFD_PERFORMANCE_BUILD_DIR;
335        $build_url  = NFD_PERFORMANCE_BUILD_URL;
336        $asset_file = $build_dir . '/performance/performance.min.asset.php';
337
338        if ( is_readable( $asset_file ) ) {
339            $asset = include_once $asset_file;
340
341            wp_register_script(
342                self::PAGE_SLUG,
343                $build_url . '/performance/performance.min.js',
344                $asset['dependencies'],
345                $asset['version'],
346                true
347            );
348
349            wp_register_style(
350                self::PAGE_SLUG,
351                $build_url . '/performance/performance.min.css',
352                array(),
353                $asset['version']
354            );
355
356            $screen = \get_current_screen();
357            if (
358                isset( $screen->id ) &&
359                (
360                    false !== strpos( $screen->id, self::PAGE_SLUG ) ||
361                    false !== strpos( $screen->id, 'tools' ) ||
362                    false !== strpos( $screen->id, container()->plugin()->id )
363                )
364            ) {
365                wp_enqueue_script( self::PAGE_SLUG );
366                wp_enqueue_style( self::PAGE_SLUG );
367            }
368        }
369    }
370
371    /**
372     * Check if is possible initialize the LazyLoading module
373     *
374     * @param bool $initialized Is initialized.
375     *
376     * @return bool
377     */
378    public function initialize_lazy_loader( $initialized ) {
379        if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'bh_pdf_invoices_' ) !== false ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 
380            $initialized = false;
381        }
382        return $initialized;
383    }
384
385    /**
386     * Register dummy performance menu page for redirect purposes
387     */
388    public static function add_dummy_performance_menu_link() {
389        add_submenu_page(
390            '', // Using empty string as parent, so it won't appear in any menu
391            'Old Performance',
392            '',
393            'manage_options',
394            self::PAGE_SLUG,
395            array( __CLASS__, 'old_performance_redirect' )
396        );
397    }
398
399    /**
400     * Handle performance redirect from old URL.
401     * This runs on admin_init to catch the redirect before headers are sent.
402     *
403     * @return void
404     */
405    public static function handle_performance_redirect() {
406        if (
407            is_admin() &&
408            isset( $_GET['page'] ) &&
409            self::PAGE_SLUG === $_GET['page']
410        ) {
411            $new_url = admin_url( 'admin.php?page=' . container()->plugin()->id . '#/settings/performance' );
412            wp_safe_redirect( $new_url );
413            exit;
414        }
415    }
416
417    /**
418     * Redirects the user to the new performance page.
419     * This is the callback for the dummy menu page.
420     *
421     * @return void
422     */
423    public static function old_performance_redirect() {
424        // Fallback: redirect using JavaScript if headers already sent
425        $new_url = admin_url( 'admin.php?page=' . container()->plugin()->id . '#/settings/performance' );
426        ?>
427        <script>
428            window.location.href = '<?php echo esc_js( $new_url ); ?>';
429        </script>
430        <p>Redirecting to new performance page...</p>
431        <?php
432    }
433}