Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.90% covered (success)
95.90%
117 / 122
76.92% covered (warning)
76.92%
10 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Admin
95.90% covered (success)
95.90%
117 / 122
76.92% covered (warning)
76.92%
10 / 13
34
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 handle_admin_menu
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 is_modern_dashboard
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_modernization_enabled
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 maybe_load_wp_build
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 dashboard_build_is_available
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 alias_screen_id_for_wp_build
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 restore_screen_id_after_wp_build
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 admin_init
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 enqueue_scripts
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
5
 plugin_page_settings_link
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 localize_api_settings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 render_settings
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The admin-specific functionality of the plugin.
4 *
5 * @since      1.0.0
6 * @package    automattic/jetpack-boost
7 */
8
9namespace Automattic\Jetpack_Boost\Admin;
10
11use Automattic\Jetpack\Admin_UI\Admin_Menu;
12use Automattic\Jetpack\Assets;
13use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
14use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
15use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
16use Automattic\Jetpack_Boost\Lib\Analytics;
17use Automattic\Jetpack_Boost\Lib\Debug;
18use Automattic\Jetpack_Boost\Lib\Environment_Change_Detector;
19use Automattic\Jetpack_Boost\Lib\Premium_Features;
20use Automattic\Jetpack_Boost\Modules\Modules_Setup;
21
22class Admin {
23    /**
24     * Menu slug.
25     */
26    const MENU_SLUG = 'jetpack-boost';
27
28    /**
29     * Filter controlling the modern dashboard; return false to restore the legacy dashboard.
30     */
31    const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_boost';
32
33    /**
34     * Whether this request loaded the modern dashboard.
35     *
36     * @var bool
37     */
38    private $modern_dashboard_loaded = false;
39
40    /**
41     * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
42     *
43     * @var string|null
44     */
45    private $wp_build_original_screen_id = null;
46
47    public function init( Modules_Setup $modules ) {
48        Environment_Change_Detector::init();
49
50        // Initiate speed scores.
51        new Speed_Score( $modules->get_ready_active_optimization_modules(), 'boost-plugin' );
52
53        add_action( 'init', array( new Analytics(), 'init' ) );
54        add_filter( 'plugin_action_links_' . JETPACK_BOOST_PLUGIN_BASE, array( $this, 'plugin_page_settings_link' ) );
55        add_action( 'admin_menu', array( $this, 'handle_admin_menu' ), 1 ); // Akismet uses 4, so we use 1 to ensure both menus are added when only they exist.
56    }
57
58    public function handle_admin_menu() {
59        $this->maybe_load_wp_build();
60
61        /**
62         * Filters the number of problems shown in the Boost sidebar menu
63         *
64         * @param int $count the number of problems shown.
65         *
66         * @since   1.0.0
67         */
68        // Only report the count to users who can actually reach the Boost menu
69        // (added below with the 'manage_options' cap). Otherwise the central
70        // menu-badges total would include problems the current user can't see.
71        if ( current_user_can( 'manage_options' ) ) {
72            $total_problems = apply_filters( 'jetpack_boost_total_problem_count', 0 );
73            \Automattic\Jetpack\Menu_Badges\Menu_Badges::init(); // idempotent; wires the renderer.
74            \Automattic\Jetpack\Menu_Badges\Notification_Counts::register(
75                'jetpack-boost',
76                array(
77                    'menu_slug' => JETPACK_BOOST_SLUG,
78                    'count'     => (int) $total_problems,
79                    'type'      => 'count',
80                )
81            );
82        }
83
84        $page_suffix = Admin_Menu::add_menu(
85            __( 'Jetpack Boost - Settings', 'jetpack-boost' ),
86            'Boost', // "Boost" is a product name, do not translate.
87            'manage_options',
88            JETPACK_BOOST_SLUG,
89            $this->modern_dashboard_loaded ? 'jetpack_boost_jetpack_boost_dashboard_wp_admin_render_page' : array( $this, 'render_settings' ),
90            null,
91            array(
92                'product' => 'boost',
93                'key'     => 'jetpack-boost',
94            )
95        );
96        add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
97    }
98
99    /**
100     * Whether this admin request uses the modern dashboard.
101     *
102     * @return bool Whether modernization is enabled for this admin request.
103     */
104    private static function is_modern_dashboard() {
105        return self::is_modernization_enabled() && is_admin();
106    }
107
108    /**
109     * Whether the modern dashboard is enabled.
110     *
111     * @return bool Whether modernization is enabled.
112     */
113    public static function is_modernization_enabled() {
114        // Keep the call below on one line: replace-next-version-tag.sh only rewrites the version there.
115        $message = __( 'The legacy Jetpack Boost dashboard will be removed in a future release.', 'jetpack-boost' );
116
117        /**
118         * Filters whether to load the modern Boost dashboard.
119         *
120         * Hook `__return_false` to restore the legacy dashboard.
121         *
122         * @since 4.7.1
123         * @since 4.8.0 Defaults to true.
124         * @deprecated $$next-version$$ The legacy dashboard will be removed in a future release.
125         * @param bool $enabled Whether to enable the modern dashboard. Default true.
126         */
127        return (bool) apply_filters_deprecated( self::MODERNIZATION_FILTER, array( true ), '$$next-version$$', '', $message );
128    }
129
130    /**
131     * Load the modern dashboard only on a Boost admin request that has not filtered it off.
132     */
133    private function maybe_load_wp_build() {
134        // Check the page first, so a site hooking the deprecated filter is only warned on Boost's screen.
135        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
136        $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
137        if ( JETPACK_BOOST_SLUG !== $page || ! self::is_modern_dashboard() || ! $this->dashboard_build_is_available() ) {
138            return;
139        }
140
141        WP_Build_Polyfills::register(
142            'jetpack-boost',
143            array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
144        );
145
146        // wp_default_scripts has already fired by admin_menu, so register the init module now.
147        jetpack_boost_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Defined by the generated build and checked in dashboard_build_is_available().
148        $this->modern_dashboard_loaded = true;
149    }
150
151    /**
152     * Load the generated dashboard, retaining the legacy page when assets are absent.
153     *
154     * @return bool Whether the generated dashboard is available.
155     */
156    private function dashboard_build_is_available() {
157        $build_file = JETPACK_BOOST_DIR_PATH . '/build/build.php';
158        if ( ! file_exists( $build_file ) ) {
159            Debug::log( 'Modern dashboard build is missing; loading the legacy dashboard.' );
160            return false;
161        }
162
163        // Hooked around the require, so only the generated enqueue callback, with its init action and REST preloading, sees the alias.
164        add_action( 'admin_enqueue_scripts', array( $this, 'alias_screen_id_for_wp_build' ) );
165        require_once $build_file;
166        add_action( 'admin_enqueue_scripts', array( $this, 'restore_screen_id_after_wp_build' ) );
167
168        if ( function_exists( 'jetpack_boost_register_script_modules' )
169            && function_exists( 'jetpack_boost_jetpack_boost_dashboard_wp_admin_render_page' ) ) {
170            return true;
171        }
172
173        remove_action( 'admin_enqueue_scripts', array( $this, 'alias_screen_id_for_wp_build' ) );
174        remove_action( 'admin_enqueue_scripts', array( $this, 'restore_screen_id_after_wp_build' ) );
175        return false;
176    }
177
178    /**
179     * Match wp-build's enqueue screen without changing the Boost menu URL.
180     *
181     * @since 4.7.1
182     * @since 4.8.0 Reads the current screen itself and records the ID it replaces.
183     */
184    public function alias_screen_id_for_wp_build() {
185        $screen = get_current_screen();
186        if ( ! $screen ) {
187            return;
188        }
189
190        $this->wp_build_original_screen_id = $screen->id;
191        $screen->id                        = 'jetpack-boost-dashboard';
192    }
193
194    /**
195     * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
196     *
197     * @since 4.8.0
198     */
199    public function restore_screen_id_after_wp_build() {
200        $screen = get_current_screen();
201        if ( ! $screen || null === $this->wp_build_original_screen_id ) {
202            return;
203        }
204
205        $screen->id                        = $this->wp_build_original_screen_id;
206        $this->wp_build_original_screen_id = null;
207    }
208
209    /**
210     * Enqueue scripts and styles for the admin page.
211     */
212    public function admin_init() {
213        // Clear premium features cache when the plugin settings page is loaded.
214        Premium_Features::clear_cache();
215        if ( current_user_can( 'manage_options' ) ) {
216            $already_activated = get_option( Premium_Features::CLOUD_CSS_ACTIVATED_OPTION );
217            Premium_Features::enable_cloud_css_after_upgrade();
218            if ( $this->modern_dashboard_loaded && ! $already_activated && get_option( Premium_Features::CLOUD_CSS_ACTIVATED_OPTION ) ) {
219                jetpack_boost_ds_set( 'cloud_css_upgrade_notice', true );
220            }
221        }
222
223        add_action( 'admin_enqueue_scripts', array( My_Jetpack_Initializer::class, 'enqueue_scripts' ) );
224        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
225    }
226
227    /**
228     * Register the JavaScript for the admin area.
229     *
230     * @since    1.0.0
231     */
232    public function enqueue_scripts() {
233        /**
234         * Filters the internal path to the distributed assets used by the plugin
235         *
236         * @param string $path the path to the assets
237         */
238        $internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' );
239
240        $admin_js_handle = 'jetpack-boost-admin';
241
242        $admin_js_dependencies = array(
243            'wp-i18n',
244            'wp-components',
245            'my_jetpack_main_app',
246        );
247
248        Assets::register_script(
249            $admin_js_handle,
250            $internal_path . 'jetpack-boost.js',
251            JETPACK_BOOST_PATH,
252            array(
253                'dependencies' => $admin_js_dependencies,
254                'in_footer'    => true,
255                'textdomain'   => 'jetpack-boost',
256                'css_path'     => $internal_path . 'jetpack-boost.css',
257            )
258        );
259
260        wp_localize_script(
261            $admin_js_handle,
262            'Jetpack_Boost',
263            ( new Config() )->constants()
264        );
265
266        Assets::enqueue_script( $admin_js_handle );
267
268        if ( $this->modern_dashboard_loaded ) {
269            $this->localize_api_settings( $admin_js_handle );
270            $i18n_loader_registered = wp_script_is( 'wp-jp-i18n-loader', 'registered' );
271            if ( $i18n_loader_registered ) {
272                wp_enqueue_script( 'wp-jp-i18n-loader' );
273            }
274
275            // The webpack handle carries Boost constants and DataSync bootstrap needed before modules run.
276            $prerequisites = wp_scripts()->query( 'jetpack-boost-dashboard-wp-admin-prerequisites', 'registered' );
277            if ( $prerequisites ) {
278                $prerequisites->deps[] = $admin_js_handle;
279                if ( $i18n_loader_registered ) {
280                    $prerequisites->deps[] = 'wp-jp-i18n-loader';
281                }
282            } else {
283                Debug::log( 'Modern dashboard prerequisites are not registered; bootstrap dependencies could not be attached.' );
284            }
285        }
286    }
287
288    /**
289     * Get settings link.
290     *
291     * @param array $links the array of links.
292     */
293    public function plugin_page_settings_link( $links ) {
294        $settings_link = '<a href="' . admin_url( 'admin.php?page=jetpack-boost' ) . '">' . esc_html__( 'Settings', 'jetpack-boost' ) . '</a>';
295        array_unshift( $links, $settings_link );
296
297        return $links;
298    }
299
300    /**
301     * Localize the REST API settings for a script.
302     *
303     * @param string $handle Script handle.
304     */
305    private function localize_api_settings( $handle ) {
306        wp_localize_script(
307            $handle,
308            'wpApiSettings',
309            array(
310                'root'  => esc_url_raw( rest_url() ),
311                'nonce' => wp_create_nonce( 'wp_rest' ),
312            )
313        );
314    }
315
316    /**
317     * Generate the settings page.
318     */
319    public function render_settings() {
320        $this->localize_api_settings( 'jetpack-boost-admin' );
321        ?>
322        <div id="jb-admin-settings"></div>
323        <?php
324    }
325}