Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.84% covered (danger)
46.84%
37 / 79
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Social_Admin_Page
46.84% covered (danger)
46.84%
37 / 79
16.67% covered (danger)
16.67%
2 / 12
196.64
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
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 add_menu
0.00% covered (danger)
0.00%
0 / 19
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
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
7.10
 load_wp_build_with_screen_alias
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
2.06
 alias_screen_id_for_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 restore_screen_id_after_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 is_social_admin_request
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
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     * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
36     *
37     * @var string|null
38     */
39    private static $wp_build_original_screen_id = null;
40
41    /**
42     * Initialize the class.
43     *
44     * @return Social_Admin_Page
45     */
46    public static function init() {
47        if ( ! isset( self::$instance ) ) {
48            self::$instance = new self();
49        }
50
51        return self::$instance;
52    }
53
54    /**
55     * The constructor.
56     */
57    private function __construct() {
58        // Load the wp-build dashboard at admin_menu priority 1 so its render
59        // function is defined before `add_menu` (priority 10) registers the page.
60        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
61        add_action( 'admin_menu', array( $this, 'add_menu' ) );
62    }
63
64    /**
65     * Load the wp-build dashboard on the Social admin request.
66     *
67     * Hooked to `admin_menu` priority 1 so the wp-build render function and
68     * enqueue hook are in place before `add_menu()` runs at the default priority.
69     *
70     * @return void
71     */
72    public static function maybe_load_wp_build() {
73        if ( ! self::is_social_admin_request() ) {
74            return;
75        }
76
77        self::load_wp_build_with_screen_alias();
78
79        // wp-build registers standalone modules (e.g. the init module) on
80        // wp_default_scripts, which has already fired by admin_menu. Register them
81        // directly so the init module makes it into the import map.
82        if ( function_exists( 'jetpack_social_register_script_modules' ) ) {
83            jetpack_social_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
84        }
85    }
86
87    /**
88     * Add the admin menu.
89     */
90    public function add_menu() {
91
92        // Remove the old Social menu item, if it exists.
93        Admin_Menu::remove_menu( 'jetpack-social' );
94
95        // If this isn't an admin (or someone with the capability to change the module status )
96        // and Publicize is inactive, then don't render the admin page.
97        if ( ! current_user_can( 'manage_options' ) && ! Utils::is_publicize_active() ) {
98            return;
99        }
100
101        // We don't need Jetpack connection on WP.com.
102        $needs_site_connection = ! ( new Host() )->is_wpcom_platform() && ! ( new Connection_Manager() )->is_connected();
103
104        /**
105         * If the Jetpack Social plugin is not active,
106         * we want to hide the menu if the site is not connected.
107         */
108        if ( ! defined( 'JETPACK_SOCIAL_PLUGIN_DIR' ) && $needs_site_connection ) {
109            return;
110        }
111
112        $page_suffix = Admin_Menu::add_menu(
113            /** "Jetpack Social" is a product name, do not translate. */
114            'Jetpack Social',
115            'Social',
116            'publish_posts',
117            'jetpack-social',
118            array( $this, 'render' ),
119            null,
120            array(
121                'product' => 'social',
122                'key'     => 'jetpack-social',
123            )
124        );
125
126        add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
127    }
128
129    /**
130     * Initialize the admin resources.
131     */
132    public function admin_init() {
133        // Refresh data if coming from purchase to ensure it is up to date
134        // without making API calls on every admin page load.
135        if ( isset( $_GET['refresh_plan_data'] ) ) {
136            check_admin_referer( self::REFRESH_PLAN_NONCE_ACTION );
137            if ( apply_filters( 'jetpack_social_should_refresh_plan_data', true ) ) {
138                Current_Plan::refresh_from_wpcom();
139            }
140        }
141
142        /**
143         * Use priority 20 to ensure that we can dequeue the old Social assets.
144         */
145        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 20 );
146
147        // Initialize the media library for the social image generator.
148        wp_enqueue_media();
149    }
150
151    /**
152     * Render the admin page by delegating to the wp-build dashboard.
153     *
154     * `maybe_load_wp_build()` defines the render function before this fires;
155     * render nothing on an unbuilt dev checkout rather than fatal.
156     */
157    public function render() {
158        if ( function_exists( 'jetpack_social_jetpack_social_dashboard_wp_admin_render_page' ) ) {
159            // Generated by wp-build (build/pages/jetpack-social-dashboard/page-wp-admin.php), so phan can't see it.
160            // @phan-suppress-next-line PhanUndeclaredFunction
161            jetpack_social_jetpack_social_dashboard_wp_admin_render_page();
162        }
163    }
164
165    /**
166     * Enqueue admin scripts and styles.
167     */
168    public function enqueue_admin_scripts() {
169        /*
170         * wp-build owns its own enqueue pipeline. The chassis reads connection
171         * state via `useConnection()`, which has no REST resolver, so hydrate it
172         * inline onto the prerequisites script that loads before the chassis module.
173         */
174        if ( wp_script_is( 'jetpack-social-dashboard-wp-admin-prerequisites', 'registered' ) ) {
175            Connection_Initial_State::render_script( 'jetpack-social-dashboard-wp-admin-prerequisites' );
176        }
177
178        // The i18n loader is registered on every admin page by jetpack-assets but
179        // only enqueued when depended on; the esbuild bundles don't pull it in.
180        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
181            wp_enqueue_script( 'wp-jp-i18n-loader' );
182        }
183    }
184
185    /**
186     * Load the wp-build entry file and register its polyfills.
187     *
188     * Only called on `?page=jetpack-social` admin requests. Keeps wp-build off
189     * every other request.
190     *
191     * @return void
192     */
193    private static function load_wp_build() {
194        $build_index = dirname( __DIR__ ) . '/build/build.php';
195
196        if ( ! file_exists( $build_index ) ) {
197            return;
198        }
199
200        require_once $build_index;
201
202        // The wp-build dashboard (unlike the Social bundles) uses the full polyfill set:
203        // the @wordpress/boot|route|a11y modules, wp-notices, wp-views, etc.
204        if ( ! class_exists( '\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills' ) ) {
205            return;
206        }
207
208        \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
209            'jetpack-social',
210            array_merge(
211                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
212                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
213            )
214        );
215    }
216
217    /**
218     * Load wp-build with the screen ID aliased across its generated enqueue check.
219     *
220     * @see WP_Build_Screen_Id::load_with_alias()
221     * @return void
222     */
223    private static function load_wp_build_with_screen_alias() {
224        // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
225        if ( method_exists( \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
226            \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id::load_with_alias(
227                array( __CLASS__, 'alias_screen_id_for_wp_build' ),
228                array( __CLASS__, 'restore_screen_id_after_wp_build' ),
229                function () {
230                    self::load_wp_build();
231                }
232            );
233            return;
234        }
235
236        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
237        self::load_wp_build();
238        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
239    }
240
241    /**
242     * Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
243     *
244     * The wp-build `<page>-wp-admin` enqueue callback fires only when the screen ID
245     * matches the wp-build page slug (`jetpack-social-dashboard`). Our wp-admin menu
246     * slug stays `jetpack-social`, so we mutate the screen object in place to make
247     * the check pass without changing the user-facing URL.
248     *
249     * Hooked only when we're on the Social admin page, so this never affects any
250     * other request.
251     *
252     * @since 0.87.1 Takes no argument; hooked on `admin_enqueue_scripts`.
253     *
254     * @return void
255     */
256    public static function alias_screen_id_for_wp_build() {
257        $screen = get_current_screen();
258        if ( ! $screen ) {
259            return;
260        }
261
262        self::$wp_build_original_screen_id = $screen->id;
263        $screen->id                        = 'jetpack-social-dashboard';
264    }
265
266    /**
267     * Undo alias_screen_id_for_wp_build(), so code after the generated check sees the real screen ID.
268     *
269     * @since 0.87.1
270     *
271     * @return void
272     */
273    public static function restore_screen_id_after_wp_build() {
274        $screen = get_current_screen();
275        if ( ! $screen || null === self::$wp_build_original_screen_id ) {
276            return;
277        }
278
279        $screen->id                        = self::$wp_build_original_screen_id;
280        self::$wp_build_original_screen_id = null;
281    }
282
283    /**
284     * Returns true when the current request targets the Social admin page.
285     *
286     * Used to scope wp-build loading to the one page that needs it. The
287     * `$_GET['page']` value is populated by wp-admin/admin.php before any of
288     * our hooks fire, so this check is reliable from the constructor onwards.
289     *
290     * @return bool
291     */
292    private static function is_social_admin_request() {
293        if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
294            return false;
295        }
296
297        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'jetpack-social'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
298    }
299}