PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-podcast/src/class-admin-page.php +384 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,384 @@
1 +<?php
2 +/**
3 + * Registers the Jetpack Podcast wp-admin page and loads the wp-build dashboard.
4 + *
5 + * @package automattic/jetpack-podcast
6 + */
7 +
8 +namespace Automattic\Jetpack\Podcast;
9 +
10 +use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 +use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 +use Automattic\Jetpack\Status\Host;
13 +use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
14 +use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
15 +
16 +/**
17 + * Adds the "Jetpack > Podcast" wp-admin screen.
18 + */
19 +class Admin_Page {
20 +
21 + const ADMIN_PAGE_SLUG = 'jetpack-podcast';
22 +
23 + /**
24 + * Where the Podcast item used to sit in the Jetpack submenu on self-hosted.
25 + *
26 + * Unread since Podcast registers without a position; kept so consumers do not fatal.
27 + *
28 + * @deprecated 2.1.1
29 + */
30 + const MENU_POSITION = 11;
31 +
32 + /**
33 + * Slug emitted by `@wordpress/build`. wp-build's auto-generated enqueue
34 + * callback only fires when `$screen->id` matches this value, so we alias
35 + * the screen id around that check without changing the user-facing URL.
36 + */
37 + const WP_BUILD_SLUG = 'jetpack-podcast-dashboard';
38 +
39 + /**
40 + * Whether `init()` has already wired its hooks.
41 + *
42 + * @var bool
43 + */
44 + private static $initialized = false;
45 +
46 + /**
47 + * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
48 + *
49 + * @var string|null
50 + */
51 + private static $wp_build_original_screen_id = null;
52 +
53 + /**
54 + * The dashboard screen hide_jitms_on_wp_build_dashboard() opts out of JITMs.
55 + *
56 + * @var string|null
57 + */
58 + private static $jitm_opt_out_screen_id = null;
59 +
60 + /**
61 + * Wire admin hooks. Idempotent.
62 + */
63 + public static function init() {
64 + if ( self::$initialized ) {
65 + return;
66 + }
67 + self::$initialized = true;
68 +
69 + add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
70 +
71 + // On Simple/Atomic, wpcom-admin-menu.php builds the Jetpack menu at
72 + // priority 999999 and calls add_wp_admin_submenu() itself. Self-hosted
73 + // has no such file, so we register our own. Priority 999 queues the item
74 + // before Admin_Menu's priority-1000 callback.
75 + if ( ! ( new Host() )->is_wpcom_platform() ) {
76 + add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ), 999 );
77 + }
78 + }
79 +
80 + /**
81 + * Register the Podcast submenu under the Jetpack menu.
82 + */
83 + public static function add_wp_admin_submenu() {
84 + // Prefer the wp-build render function once it's defined (by
85 + // maybe_load_wp_build() at admin_menu priority 1); fall back otherwise.
86 + $wp_build_render = 'jetpack_podcast_jetpack_podcast_dashboard_wp_admin_render_page';
87 + $callback = function_exists( $wp_build_render ) ? $wp_build_render : array( __CLASS__, 'render' );
88 +
89 + if ( ( new Host() )->is_wpcom_platform() ) {
90 + $page_suffix = add_submenu_page(
91 + 'jetpack',
92 + /** "Podcast" is a product name, do not translate. */
93 + 'Podcast',
94 + 'Podcast',
95 + 'manage_options',
96 + self::ADMIN_PAGE_SLUG,
97 + $callback
98 + );
99 + } else {
100 + $page_suffix = Admin_Menu::add_menu(
101 + /** "Podcast" is a product name, do not translate. */
102 + 'Podcast',
103 + 'Podcast',
104 + 'manage_options',
105 + self::ADMIN_PAGE_SLUG,
106 + $callback,
107 + null,
108 + // Podcast has no My Jetpack product class, so the module is the only gate available.
109 + array(
110 + 'module' => 'podcast',
111 + 'key' => 'jetpack-podcast',
112 + )
113 + );
114 + }
115 +
116 + if ( $page_suffix ) {
117 + add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
118 + self::opt_out_of_jitms( $page_suffix );
119 + }
120 + }
121 +
122 + /**
123 + * Wire admin-init actions once we know the Podcast page is loading.
124 + */
125 + public static function admin_init() {
126 + // MediaUpload (cover-image-control) reads wp.media.view — only defined after this runs.
127 + add_action( 'admin_enqueue_scripts', 'wp_enqueue_media' );
128 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_tracks_transport' ) );
129 + }
130 +
131 + /**
132 + * Load the Tracks transport for the dashboard's client-side events.
133 + *
134 + * `jetpackAnalytics.tracks.recordEvent()` only pushes onto `window._tkq`,
135 + * which stays an inert array until `w.js` loads and drains it. Nothing
136 + * supplies that on Atomic or self-hosted, so without this the queue grows
137 + * for the life of the page. Simple is skipped because stats.php already
138 + * prints the same script on `admin_footer`, and loading it twice would
139 + * re-drain a queue that has already been flushed.
140 + */
141 + public static function enqueue_tracks_transport() {
142 + if ( ( new Host() )->is_wpcom_simple() ) {
143 + return;
144 + }
145 +
146 + wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
147 + }
148 +
149 + /**
150 + * Hooked at admin_menu priority 1 so polyfills register before
151 + * `wp_default_scripts` fires and the wp-build render function is defined
152 + * before `add_wp_admin_submenu()` runs (priority 999 on self-hosted, 999999
153 + * on Simple/Atomic).
154 + */
155 + public static function maybe_load_wp_build() {
156 + if ( ! self::is_podcast_admin_request() ) {
157 + return;
158 + }
159 +
160 + self::load_wp_build_with_screen_alias();
161 + add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'inject_podcast_script_data' ) );
162 + }
163 +
164 + /**
165 + * Add the podcast gate boolean to `window.JetpackScriptData`.
166 + *
167 + * Hooked from `maybe_load_wp_build()` so it only runs when the request is
168 + * for the podcast admin page.
169 + *
170 + * @param array $data Script data being injected.
171 + * @return array
172 + */
173 + public static function inject_podcast_script_data( $data ) {
174 + if ( ! is_array( $data ) ) {
175 + $data = array();
176 + }
177 +
178 + $is_wpcom = ( new Host() )->is_wpcom_platform();
179 +
180 + if ( ! $is_wpcom && empty( $data['site']['wpcom']['blog_id'] ) ) {
181 + $blog_id = (int) Connection_Manager::get_site_id( true );
182 + if ( $blog_id > 0 ) {
183 + $data['site']['wpcom']['blog_id'] = $blog_id;
184 + }
185 + }
186 +
187 + // Self-hosted upsells the Growth plan; WordPress.com keeps Premium.
188 + // `product_slug` is fed straight to the checkout URL; `plan_name` is a
189 + // product name shown in the locked-preview copy (not translated).
190 + $data['podcast'] = array(
191 + 'has_product_access' => Podcast_Gate::has_product_access(),
192 + 'is_connected' => $is_wpcom || ( new Connection_Manager( 'jetpack' ) )->is_connected(),
193 + 'show_url_hosts' => Settings::SHOW_URL_HOSTS,
194 + 'show_url_max_length' => Settings::SHOW_URL_MAX_LENGTH,
195 + 'feed_limit_max' => Settings::feed_limit_max(),
196 + 'preload' => rest_preload_api_request( array(), '/wpcom/v2/podcast/settings' ),
197 + 'selected_category' => self::get_selected_category(),
198 + 'tracks_user_data' => self::get_tracks_user_data(),
199 + 'upgrade' => array(
200 + 'product_slug' => $is_wpcom ? 'premium' : 'jetpack_growth_yearly',
201 + 'plan_name' => $is_wpcom ? 'Premium' : 'Growth',
202 + ),
203 + );
204 +
205 + return $data;
206 + }
207 +
208 + /**
209 + * Connected-user identity for Tracks, so client events aren't anonymous on
210 + * Atomic and self-hosted. Null on Simple, where stats.php already pushes
211 + * `identifyUser` before our bundle runs.
212 + *
213 + * Deliberately narrower than `get_connected_user_tracks_identity()`, which
214 + * also returns email, blogid and locale — none of which Tracks needs here.
215 + *
216 + * @return array{userid:mixed, username:mixed}|null
217 + */
218 + private static function get_tracks_user_data() {
219 + if ( ! class_exists( 'Jetpack_Tracks_Client' ) ) {
220 + return null;
221 + }
222 +
223 + $identity = \Jetpack_Tracks_Client::get_connected_user_tracks_identity();
224 + if ( ! is_array( $identity ) || ! isset( $identity['userid'] ) || ! isset( $identity['username'] ) ) {
225 + return null;
226 + }
227 +
228 + return array(
229 + 'userid' => $identity['userid'],
230 + 'username' => $identity['username'],
231 + );
232 + }
233 +
234 + /**
235 + * The currently designated podcast category, injected so the settings
236 + * picker can label its selected option on first paint instead of waiting on
237 + * the client-side taxonomy→terms fetch. The full list still loads lazily.
238 + *
239 + * @return array{id:int, name:string}|null Null when no category is set.
240 + */
241 + public static function get_selected_category() {
242 + $category_id = (int) get_option( 'podcasting_category_id', 0 );
243 + if ( $category_id <= 0 ) {
244 + return null;
245 + }
246 +
247 + $term = get_term( $category_id, 'category' );
248 + if ( ! $term instanceof \WP_Term ) {
249 + return null;
250 + }
251 +
252 + return array(
253 + 'id' => (int) $term->term_id,
254 + 'name' => $term->name,
255 + );
256 + }
257 +
258 + /**
259 + * The build artifact may be absent on a fresh checkout before
260 + * `pnpm build` has run; in that case `add_wp_admin_submenu()` falls back
261 + * to `render()` so the page still loads (just without the React app).
262 + */
263 + private static function load_wp_build() {
264 + $build_index = dirname( __DIR__ ) . '/build/build.php';
265 +
266 + if ( ! file_exists( $build_index ) ) {
267 + return;
268 + }
269 +
270 + require_once $build_index;
271 +
272 + WP_Build_Polyfills::register(
273 + 'jetpack-podcast',
274 + array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
275 + );
276 + }
277 +
278 + /**
279 + * Load wp-build with the screen ID aliased across its generated enqueue check.
280 + *
281 + * @see WP_Build_Screen_Id::load_with_alias()
282 + * @return void
283 + */
284 + private static function load_wp_build_with_screen_alias() {
285 + // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
286 + if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
287 + WP_Build_Screen_Id::load_with_alias(
288 + array( __CLASS__, 'alias_screen_id_for_wp_build' ),
289 + array( __CLASS__, 'restore_screen_id_after_wp_build' ),
290 + function () {
291 + self::load_wp_build();
292 + }
293 + );
294 + return;
295 + }
296 +
297 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
298 + self::load_wp_build();
299 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
300 + }
301 +
302 + /**
303 + * Alias the current screen id to wp-build's expected slug.
304 + *
305 + * @since 2.1.3 Takes no argument; hooked on `admin_enqueue_scripts`.
306 + */
307 + public static function alias_screen_id_for_wp_build() {
308 + $screen = get_current_screen();
309 + if ( ! $screen ) {
310 + return;
311 + }
312 +
313 + self::$wp_build_original_screen_id = $screen->id;
314 + $screen->id = self::WP_BUILD_SLUG;
315 + }
316 +
317 + /**
318 + * Undo alias_screen_id_for_wp_build(), so code after the generated check sees the real screen ID.
319 + *
320 + * @since 2.1.3
321 + */
322 + public static function restore_screen_id_after_wp_build() {
323 + $screen = get_current_screen();
324 + if ( ! $screen || null === self::$wp_build_original_screen_id ) {
325 + return;
326 + }
327 +
328 + $screen->id = self::$wp_build_original_screen_id;
329 + self::$wp_build_original_screen_id = null;
330 + }
331 +
332 + /**
333 + * Opt the dashboard's screen out of JITMs.
334 + *
335 + * @param string $screen_id The hook suffix the page was registered under, which is its screen ID.
336 + */
337 + private static function opt_out_of_jitms( $screen_id ) {
338 + self::$jitm_opt_out_screen_id = $screen_id;
339 + add_filter( 'jetpack_display_jitms_on_screen', array( __CLASS__, 'hide_jitms_on_wp_build_dashboard' ), 10, 2 );
340 + }
341 +
342 + /**
343 + * Keep JITMs off the wp-build dashboard, which has no `#jp-admin-notices` to show them in.
344 + *
345 + * Fetching a JITM records a view, so one the page hides would still be counted.
346 + *
347 + * @since 2.1.3
348 + *
349 + * @param bool $show Whether to show JITMs on the screen.
350 + * @param string $screen_id The screen ID.
351 + * @return bool
352 + */
353 + public static function hide_jitms_on_wp_build_dashboard( $show, $screen_id ) {
354 + if ( null !== self::$jitm_opt_out_screen_id && self::$jitm_opt_out_screen_id === $screen_id ) {
355 + return false;
356 + }
357 +
358 + return $show;
359 + }
360 +
361 + /**
362 + * Fallback render used when the wp-build artifact is missing.
363 + */
364 + public static function render() {
365 + ?>
366 + <div class="wrap">
367 + <h1>Podcast</h1>
368 + </div>
369 + <?php
370 + }
371 +
372 + /**
373 + * Whether the current request targets the Podcast admin page.
374 + */
375 + private static function is_podcast_admin_request() {
376 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
377 + if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
378 + return false;
379 + }
380 +
381 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
382 + return self::ADMIN_PAGE_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) );
383 + }
384 +}