Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
17.86% covered (danger)
17.86%
10 / 56
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Social_Admin_Page
17.86% covered (danger)
17.86%
10 / 56
0.00% covered (danger)
0.00%
0 / 10
462.54
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 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 add_menu
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 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
6
 enqueue_admin_scripts
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 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_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\Connection\Initial_State as Connection_Initial_State;
12use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13use Automattic\Jetpack\Current_Plan;
14use Automattic\Jetpack\Publicize\Publicize_Utils as Utils;
15use Automattic\Jetpack\Status\Host;
16
17/**
18 * The class to handle the Social Admin Page.
19 */
20class Social_Admin_Page {
21
22    /**
23     * Nonce action used when refreshing plan data.
24     */
25    public const REFRESH_PLAN_NONCE_ACTION = 'jetpack_social_refresh_plan_data';
26
27    /**
28     * The instance of the class.
29     *
30     * @var Social_Admin_Page
31     */
32    private static $instance;
33
34    /**
35     * Initialize the class.
36     *
37     * @return Social_Admin_Page
38     */
39    public static function init() {
40        if ( ! isset( self::$instance ) ) {
41            self::$instance = new self();
42        }
43
44        return self::$instance;
45    }
46
47    /**
48     * The constructor.
49     */
50    private function __construct() {
51        // Load the wp-build dashboard at admin_menu priority 1 so its render
52        // function is defined before `add_menu` (priority 10) registers the page.
53        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
54        add_action( 'admin_menu', array( $this, 'add_menu' ) );
55    }
56
57    /**
58     * Load the wp-build dashboard on the Social admin request.
59     *
60     * Hooked to `admin_menu` priority 1 so the wp-build render function and
61     * enqueue hook are in place before `add_menu()` runs at the default priority.
62     *
63     * @return void
64     */
65    public static function maybe_load_wp_build() {
66        if ( ! self::is_social_admin_request() ) {
67            return;
68        }
69
70        self::load_wp_build();
71
72        // wp-build registers standalone modules (e.g. the init module) on
73        // wp_default_scripts, which has already fired by admin_menu. Register them
74        // directly so the init module makes it into the import map.
75        if ( function_exists( 'jetpack_social_register_script_modules' ) ) {
76            jetpack_social_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
77        }
78
79        add_action( 'current_screen', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
80    }
81
82    /**
83     * Add the admin menu.
84     */
85    public function add_menu() {
86
87        // Remove the old Social menu item, if it exists.
88        Admin_Menu::remove_menu( 'jetpack-social' );
89
90        // If this isn't an admin (or someone with the capability to change the module status )
91        // and Publicize is inactive, then don't render the admin page.
92        if ( ! current_user_can( 'manage_options' ) && ! Utils::is_publicize_active() ) {
93            return;
94        }
95
96        // We don't need Jetpack connection on WP.com.
97        $needs_site_connection = ! ( new Host() )->is_wpcom_platform() && ! ( new Connection_Manager() )->is_connected();
98
99        /**
100         * If the Jetpack Social plugin is not active,
101         * we want to hide the menu if the site is not connected.
102         */
103        if ( ! defined( 'JETPACK_SOCIAL_PLUGIN_DIR' ) && $needs_site_connection ) {
104            return;
105        }
106
107        $page_suffix = Admin_Menu::add_menu(
108            /** "Jetpack Social" is a product name, do not translate. */
109            'Jetpack Social',
110            'Social',
111            'publish_posts',
112            'jetpack-social',
113            array( $this, 'render' )
114        );
115
116        add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
117    }
118
119    /**
120     * Initialize the admin resources.
121     */
122    public function admin_init() {
123        // Refresh data if coming from purchase to ensure it is up to date
124        // without making API calls on every admin page load.
125        if ( isset( $_GET['refresh_plan_data'] ) ) {
126            check_admin_referer( self::REFRESH_PLAN_NONCE_ACTION );
127            if ( apply_filters( 'jetpack_social_should_refresh_plan_data', true ) ) {
128                Current_Plan::refresh_from_wpcom();
129            }
130        }
131
132        /**
133         * Use priority 20 to ensure that we can dequeue the old Social assets.
134         */
135        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 20 );
136
137        // Initialize the media library for the social image generator.
138        wp_enqueue_media();
139    }
140
141    /**
142     * Render the admin page by delegating to the wp-build dashboard.
143     *
144     * `maybe_load_wp_build()` defines the render function before this fires;
145     * render nothing on an unbuilt dev checkout rather than fatal.
146     */
147    public function render() {
148        if ( function_exists( 'jetpack_social_jetpack_social_dashboard_wp_admin_render_page' ) ) {
149            // Generated by wp-build (build/pages/jetpack-social-dashboard/page-wp-admin.php), so phan can't see it.
150            // @phan-suppress-next-line PhanUndeclaredFunction
151            jetpack_social_jetpack_social_dashboard_wp_admin_render_page();
152        }
153    }
154
155    /**
156     * Enqueue admin scripts and styles.
157     */
158    public function enqueue_admin_scripts() {
159        /*
160         * wp-build owns its own enqueue pipeline. The chassis reads connection
161         * state via `useConnection()`, which has no REST resolver, so hydrate it
162         * inline onto the prerequisites script that loads before the chassis module.
163         */
164        if ( wp_script_is( 'jetpack-social-dashboard-wp-admin-prerequisites', 'registered' ) ) {
165            Connection_Initial_State::render_script( 'jetpack-social-dashboard-wp-admin-prerequisites' );
166        }
167
168        // The i18n loader is registered on every admin page by jetpack-assets but
169        // only enqueued when depended on; the esbuild bundles don't pull it in.
170        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
171            wp_enqueue_script( 'wp-jp-i18n-loader' );
172        }
173    }
174
175    /**
176     * Load the wp-build entry file and register its polyfills.
177     *
178     * Only called on `?page=jetpack-social` admin requests. Keeps wp-build off
179     * every other request.
180     *
181     * @return void
182     */
183    private static function load_wp_build() {
184        $build_index = dirname( __DIR__ ) . '/build/build.php';
185
186        if ( ! file_exists( $build_index ) ) {
187            return;
188        }
189
190        require_once $build_index;
191
192        // The wp-build dashboard (unlike the Social bundles) uses the full polyfill set:
193        // the @wordpress/boot|route|a11y modules, wp-notices, wp-views, etc.
194        if ( ! class_exists( '\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills' ) ) {
195            return;
196        }
197
198        \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
199            'jetpack-social',
200            array_merge(
201                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
202                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
203            )
204        );
205    }
206
207    /**
208     * Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
209     *
210     * The wp-build `<page>-wp-admin` enqueue callback fires only when the screen ID
211     * matches the wp-build page slug (`jetpack-social-dashboard`). Our wp-admin menu
212     * slug stays `jetpack-social`, so we mutate the screen object in place to make
213     * the check pass without changing the user-facing URL.
214     *
215     * Hooked only when we're on the Social admin page, so this never affects any
216     * other request.
217     *
218     * @param \WP_Screen|null $screen The current screen object (passed by WP).
219     * @return void
220     */
221    public static function alias_screen_id_for_wp_build( $screen ) {
222        if ( ! is_object( $screen ) ) {
223            return;
224        }
225
226        $screen->id = 'jetpack-social-dashboard';
227    }
228
229    /**
230     * Returns true when the current request targets the Social admin page.
231     *
232     * Used to scope wp-build loading to the one page that needs it. The
233     * `$_GET['page']` value is populated by wp-admin/admin.php before any of
234     * our hooks fire, so this check is reliable from the constructor onwards.
235     *
236     * @return bool
237     */
238    private static function is_social_admin_request() {
239        if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
240            return false;
241        }
242
243        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'jetpack-social'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
244    }
245}