Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.25% covered (danger)
34.25%
25 / 73
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Social_Admin_Page
34.25% covered (danger)
34.25%
25 / 73
16.67% covered (danger)
16.67%
2 / 12
304.20
0.00% covered (danger)
0.00%
0 / 1
 init
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_load_wp_build
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 add_menu
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 admin_init
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 render
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_admin_scripts
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 load_wp_build
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 alias_screen_id_for_wp_build
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 is_modernized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_wp_build_dashboard_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_social_admin_request
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Social Admin Page class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Assets;
12use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
13use Automattic\Jetpack\Connection\Manager as Connection_Manager;
14use Automattic\Jetpack\Current_Plan;
15use Automattic\Jetpack\Publicize\Publicize_Utils as Utils;
16use Automattic\Jetpack\Status\Host;
17
18/**
19 * The class to handle the Social Admin Page.
20 */
21class Social_Admin_Page {
22
23    /**
24     * Nonce action used when refreshing plan data.
25     */
26    public const REFRESH_PLAN_NONCE_ACTION = 'jetpack_social_refresh_plan_data';
27
28    /**
29     * Filter name that gates the wp-build–based dashboard.
30     *
31     * When this filter returns true, "Jetpack > Social" renders the new
32     * wp-build dashboard (Overview + Settings tabs) instead of the legacy
33     * single-page React app.
34     */
35    const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_social';
36
37    /**
38     * The instance of the class.
39     *
40     * @var Social_Admin_Page
41     */
42    private static $instance;
43
44    /**
45     * Initialize the class.
46     *
47     * @return Social_Admin_Page
48     */
49    public static function init() {
50        if ( ! isset( self::$instance ) ) {
51            self::$instance = new self();
52        }
53
54        return self::$instance;
55    }
56
57    /**
58     * The constructor.
59     */
60    private function __construct() {
61        // Defer wp-build loading to admin_menu (priority 1) so the modernization
62        // filter — which third parties typically register from a plugins_loaded
63        // or init callback (e.g. via Code Snippets) — has been applied before we
64        // read it, and so the wp-build render function is defined before
65        // `add_menu` (priority 10) reads `function_exists()`.
66        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
67        add_action( 'admin_menu', array( $this, 'add_menu' ) );
68    }
69
70    /**
71     * Load wp-build for the Social admin page when modernization is enabled.
72     *
73     * Hooked to `admin_menu` priority 1 so the modernization filter has been
74     * registered by any opt-in code (mu-plugins, snippets, themes) before we
75     * read it, and so the wp-build render function and enqueue hook are in
76     * place before `add_menu()` runs at the default priority.
77     *
78     * @return void
79     */
80    public static function maybe_load_wp_build() {
81        if ( ! self::is_modernized() || ! self::is_social_admin_request() ) {
82            return;
83        }
84
85        self::load_wp_build();
86        add_action( 'current_screen', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
87    }
88
89    /**
90     * Add the admin menu.
91     */
92    public function add_menu() {
93
94        // Remove the old Social menu item, if it exists.
95        Admin_Menu::remove_menu( 'jetpack-social' );
96
97        // If this isn't an admin (or someone with the capability to change the module status )
98        // and Publicize is inactive, then don't render the admin page.
99        if ( ! current_user_can( 'manage_options' ) && ! Utils::is_publicize_active() ) {
100            return;
101        }
102
103        // We don't need Jetpack connection on WP.com.
104        $needs_site_connection = ! ( new Host() )->is_wpcom_platform() && ! ( new Connection_Manager() )->is_connected();
105
106        /**
107         * If the Jetpack Social plugin is not active,
108         * we want to hide the menu if the site is not connected.
109         */
110        if ( ! defined( 'JETPACK_SOCIAL_PLUGIN_DIR' ) && $needs_site_connection ) {
111            return;
112        }
113
114        $callback = self::is_wp_build_dashboard_active()
115            ? 'jetpack_social_jetpack_social_dashboard_wp_admin_render_page'
116            : array( $this, 'render' );
117
118        $page_suffix = Admin_Menu::add_menu(
119            /** "Jetpack Social" is a product name, do not translate. */
120            'Jetpack Social',
121            'Social',
122            'publish_posts',
123            'jetpack-social',
124            $callback,
125            4
126        );
127
128        add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
129    }
130
131    /**
132     * Initialize the admin resources.
133     */
134    public function admin_init() {
135        // Refresh data if coming from purchase to ensure it is up to date
136        // without making API calls on every admin page load.
137        if ( isset( $_GET['refresh_plan_data'] ) ) {
138            check_admin_referer( self::REFRESH_PLAN_NONCE_ACTION );
139            if ( apply_filters( 'jetpack_social_should_refresh_plan_data', true ) ) {
140                Current_Plan::refresh_from_wpcom();
141            }
142        }
143
144        /**
145         * Use priority 20 to ensure that we can dequeue the old Social assets.
146         */
147        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 20 );
148
149        // Initialize the media library for the social image generator.
150        wp_enqueue_media();
151    }
152
153    /**
154     * Render the admin page.
155     */
156    public function render() {
157        ?>
158            <div id="jetpack-social-root"></div>
159        <?php
160    }
161
162    /**
163     * Enqueue admin scripts and styles.
164     */
165    public function enqueue_admin_scripts() {
166        // This callback is registered via `load-{$page_suffix}` in `add_menu()`,
167        // so it only fires on the Social admin page — no need to re-check the page here.
168        //
169        // Gate on `is_wp_build_dashboard_active()` (not `is_modernized()` alone) so
170        // this mirrors the exact decision `add_menu()` made when choosing the menu
171        // callback: when modernization is on AND the wp-build render function is defined
172        // (i.e. the chassis was actually loaded), skip the legacy bundle entirely.
173        if ( self::is_wp_build_dashboard_active() ) {
174            // wp-build manages its own enqueue pipeline. The legacy script,
175            // localized config, and media-library bootstrap are intentionally
176            // skipped here.
177            //
178            // The chassis reads site-connection state via `useConnection()`, whose
179            // store has no REST resolver — it must be hydrated inline. The wp-build
180            // boot registers a classic `…-prerequisites` script that loads before the
181            // chassis module, so attach the connection initial state there.
182            if ( wp_script_is( 'jetpack-social-dashboard-wp-admin-prerequisites', 'registered' ) ) {
183                Connection_Initial_State::render_script( 'jetpack-social-dashboard-wp-admin-prerequisites' );
184            }
185            return;
186        }
187
188        Publicize_Assets::register_wp_build_polyfills();
189
190        // Dequeue the old Social assets.
191        wp_dequeue_script( 'jetpack-social' );
192        wp_dequeue_style( 'jetpack-social' );
193
194        Assets::register_script(
195            'social-admin-page',
196            '../build/social-admin-page.js',
197            __FILE__,
198            array(
199                'in_footer'  => true,
200                'textdomain' => 'jetpack-publicize-pkg',
201                'enqueue'    => true,
202            )
203        );
204    }
205
206    /**
207     * Load the wp-build entry file and register its polyfills.
208     *
209     * Only called on `?page=jetpack-social` admin requests when the
210     * modernization filter is enabled. Keeps wp-build off every other request.
211     *
212     * @return void
213     */
214    private static function load_wp_build() {
215        $build_index = dirname( __DIR__ ) . '/build/build.php';
216
217        if ( ! file_exists( $build_index ) ) {
218            return;
219        }
220
221        require_once $build_index;
222
223        // The wp-build dashboard (unlike the Social bundles) uses the full polyfill set:
224        // the @wordpress/boot|route|a11y modules, wp-notices, wp-views, etc.
225        if ( ! class_exists( '\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills' ) ) {
226            return;
227        }
228
229        \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
230            'jetpack-social',
231            array_merge(
232                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
233                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
234            )
235        );
236    }
237
238    /**
239     * Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
240     *
241     * The wp-build `<page>-wp-admin` enqueue callback fires only when the screen ID
242     * matches the wp-build page slug (`jetpack-social-dashboard`). Our wp-admin menu
243     * slug stays `jetpack-social`, so we mutate the screen object in place to make
244     * the check pass without changing the user-facing URL.
245     *
246     * Hooked only when modernization is on AND we're on the Social admin page,
247     * so this never affects any other request.
248     *
249     * @param \WP_Screen|null $screen The current screen object (passed by WP).
250     * @return void
251     */
252    public static function alias_screen_id_for_wp_build( $screen ) {
253        if ( ! is_object( $screen ) ) {
254            return;
255        }
256
257        $screen->id = 'jetpack-social-dashboard';
258    }
259
260    /**
261     * Returns true when the wp-build modernization filter is enabled.
262     *
263     * The modernized Social dashboard now defaults on for every site. Hosts (and
264     * a11ns who want the legacy view back) can still force the legacy experience
265     * with `add_filter( self::MODERNIZATION_FILTER, '__return_false' );`.
266     *
267     * @return bool
268     */
269    private static function is_modernized() {
270        return (bool) apply_filters( self::MODERNIZATION_FILTER, true );
271    }
272
273    /**
274     * Returns true when the wp-build dashboard is the page that will actually render.
275     *
276     * This is the single source of truth shared by `add_menu()` (which picks the
277     * menu callback) and `enqueue_admin_scripts()` (which decides whether to skip
278     * the legacy bundle). `maybe_load_wp_build()` only loads the wp-build entry —
279     * and therefore only defines its render function — when modernization is on AND
280     * we are on the Social admin page. Checking `function_exists()` here captures
281     * both conditions in one place, so the callback and the enqueue gate can never
282     * diverge and leave an empty `#jetpack-social-root`.
283     *
284     * @return bool
285     */
286    private static function is_wp_build_dashboard_active() {
287        return self::is_modernized() && function_exists( 'jetpack_social_jetpack_social_dashboard_wp_admin_render_page' );
288    }
289
290    /**
291     * Returns true when the current request targets the Social admin page.
292     *
293     * Used to scope wp-build loading to the one page that needs it. The
294     * `$_GET['page']` value is populated by wp-admin/admin.php before any of
295     * our hooks fire, so this check is reliable from the constructor onwards.
296     *
297     * @return bool
298     */
299    private static function is_social_admin_request() {
300        if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
301            return false;
302        }
303
304        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'jetpack-social'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
305    }
306}