Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
81.48% |
88 / 108 |
|
61.54% |
8 / 13 |
CRAP | |
0.00% |
0 / 1 |
| Subscribers_Announcement | |
81.48% |
88 / 108 |
|
61.54% |
8 / 13 |
37.10 | |
0.00% |
0 / 1 |
| init | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| is_enabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| maybe_load_wp_build | |
14.29% |
2 / 14 |
|
0.00% |
0 / 1 |
14.08 | |||
| alias_screen_id_for_wp_build | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| add_menu | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
4 | |||
| add_wp_admin_submenu | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
4.01 | |||
| on_page_load | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| print_app_data | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
| render_fallback | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| handle_toggle_menu | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| handle_go_to_newsletter | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| tracking | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_announcement_request | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Transitional "Subscribers moved" announcement page. |
| 4 | * |
| 5 | * When the Newsletter modernization filter is enabled, the unified |
| 6 | * Jetpack β Newsletter page owns subscriber management and the legacy |
| 7 | * "Subscribers β" Calypso shortcut is retired. Instead of silently dropping |
| 8 | * the menu item, this page takes its place so people who rely on the link |
| 9 | * learn the new location before it disappears. They can also remove the |
| 10 | * menu item themselves once they have adopted the new flow. |
| 11 | * |
| 12 | * The whole feature is temporary and kept deliberately small: this class |
| 13 | * (menu, handlers, tracking) plus the `routes/subscribers-announcement` |
| 14 | * wp-build route can be deleted wholesale once the transition period ends. |
| 15 | * |
| 16 | * @package automattic/jetpack-newsletter |
| 17 | */ |
| 18 | |
| 19 | namespace Automattic\Jetpack\Newsletter; |
| 20 | |
| 21 | use Automattic\Jetpack\Admin_UI\Admin_Menu; |
| 22 | use Automattic\Jetpack\Connection\Manager as Connection_Manager; |
| 23 | use Automattic\Jetpack\Tracking; |
| 24 | |
| 25 | /** |
| 26 | * Renders the transitional Subscribers announcement page and handles its |
| 27 | * "remove from sidebar" toggle and "Take me to Newsletter" redirect. |
| 28 | * |
| 29 | * The menu itself is registered by callers that own the Subscribers menu |
| 30 | * placement (the Jetpack plugin's subscriptions module) via add_menu(); |
| 31 | * this class self-registers only request handlers and wp-build loading. |
| 32 | * |
| 33 | * @since 0.10.0 |
| 34 | */ |
| 35 | class Subscribers_Announcement { |
| 36 | |
| 37 | /** |
| 38 | * Admin page slug (kept distinct from the wp-build page name; the screen |
| 39 | * ID is aliased so wp-build's enqueue check still matches). |
| 40 | * |
| 41 | * @var string |
| 42 | */ |
| 43 | const PAGE_SLUG = 'jetpack-subscribers'; |
| 44 | |
| 45 | /** |
| 46 | * Wp-build page name, matching `routes/subscribers-announcement/package.json`. |
| 47 | * |
| 48 | * @var string |
| 49 | */ |
| 50 | const WP_BUILD_PAGE = 'jetpack-subscribers-announcement'; |
| 51 | |
| 52 | /** |
| 53 | * Option storing whether the user removed the Subscribers menu item. |
| 54 | * |
| 55 | * @var string |
| 56 | */ |
| 57 | const REMOVED_OPTION = 'jetpack_subscribers_announcement_menu_removed'; |
| 58 | |
| 59 | /** |
| 60 | * AJAX action toggling the menu item visibility. |
| 61 | * |
| 62 | * @var string |
| 63 | */ |
| 64 | const TOGGLE_ACTION = 'jetpack_subscribers_announcement_toggle_menu'; |
| 65 | |
| 66 | /** |
| 67 | * Admin-post action tracking the "Take me to Newsletter" click before redirecting. |
| 68 | * |
| 69 | * @var string |
| 70 | */ |
| 71 | const GO_ACTION = 'jetpack_subscribers_announcement_go_to_newsletter'; |
| 72 | |
| 73 | /** |
| 74 | * Register request handlers and the wp-build loader. |
| 75 | * |
| 76 | * Called from Settings::init_hooks() so the AJAX/admin-post handlers exist |
| 77 | * on admin-ajax.php / admin-post.php requests, where `admin_menu` (and so |
| 78 | * add_menu()) never fires. |
| 79 | * |
| 80 | * @return void |
| 81 | */ |
| 82 | public static function init() { |
| 83 | add_action( 'wp_ajax_' . self::TOGGLE_ACTION, array( __CLASS__, 'handle_toggle_menu' ) ); |
| 84 | add_action( 'admin_post_' . self::GO_ACTION, array( __CLASS__, 'handle_go_to_newsletter' ) ); |
| 85 | |
| 86 | // Priority 1 mirrors Settings::maybe_load_wp_build(): the modernization |
| 87 | // filter has been registered by opt-in code by then, and the wp-build |
| 88 | // render function must exist before menu callbacks are resolved. |
| 89 | add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 ); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Whether the announcement page feature is active. |
| 94 | * |
| 95 | * @return bool |
| 96 | */ |
| 97 | public static function is_enabled() { |
| 98 | // The default must match the menu-registration call sites |
| 99 | // (subscriptions.php, wpcom-admin-menu.php) and Settings::is_modernized(), |
| 100 | // which all default to `true`. If this defaulted to `false` while the menu |
| 101 | // is registered, maybe_load_wp_build() would bail, the wp-build render |
| 102 | // function would never be defined, and add_menu() would serve the bare |
| 103 | // render_fallback() page instead of the styled announcement app. |
| 104 | /** This filter is documented in projects/packages/newsletter/src/class-settings.php */ |
| 105 | return (bool) apply_filters( Settings::MODERNIZATION_FILTER, true ); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Load wp-build for the announcement page when the feature is enabled. |
| 110 | * |
| 111 | * @return void |
| 112 | */ |
| 113 | public static function maybe_load_wp_build() { |
| 114 | if ( ! self::is_enabled() || ! self::is_announcement_request() ) { |
| 115 | return; |
| 116 | } |
| 117 | |
| 118 | $build_index = dirname( __DIR__ ) . '/build/build.php'; |
| 119 | |
| 120 | if ( ! file_exists( $build_index ) ) { |
| 121 | return; |
| 122 | } |
| 123 | |
| 124 | require_once $build_index; |
| 125 | |
| 126 | \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register( |
| 127 | 'jetpack-newsletter', |
| 128 | array_merge( |
| 129 | \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES, |
| 130 | \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS |
| 131 | ) |
| 132 | ); |
| 133 | |
| 134 | add_action( 'current_screen', array( __CLASS__, 'alias_screen_id_for_wp_build' ) ); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Alias the current screen ID to satisfy wp-build's auto-generated enqueue check. |
| 139 | * |
| 140 | * Mirrors Settings::alias_screen_id_for_wp_build(): wp-build enqueues only |
| 141 | * when the screen ID matches the wp-build page name, while our menu slug |
| 142 | * stays `jetpack-subscribers`. |
| 143 | * |
| 144 | * @param \WP_Screen|null $screen The current screen object (passed by WP). |
| 145 | * @return void |
| 146 | */ |
| 147 | public static function alias_screen_id_for_wp_build( $screen ) { |
| 148 | if ( ! is_object( $screen ) ) { |
| 149 | return; |
| 150 | } |
| 151 | |
| 152 | $screen->id = self::WP_BUILD_PAGE; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Register the Subscribers announcement page under the Jetpack menu. |
| 157 | * |
| 158 | * When the user opted to remove the menu item, the page stays registered |
| 159 | * (so the page remains reachable directly and the choice can be undone) |
| 160 | * but the sidebar entry is removed. |
| 161 | * |
| 162 | * @return void |
| 163 | */ |
| 164 | public static function add_menu() { |
| 165 | $callback = function_exists( 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' ) |
| 166 | ? 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' |
| 167 | : array( __CLASS__, 'render_fallback' ); |
| 168 | |
| 169 | if ( get_option( self::REMOVED_OPTION ) ) { |
| 170 | // Register as a hidden page (empty parent slug): it stays reachable |
| 171 | // at its URL β so the choice can be undone from the page itself β |
| 172 | // but never appears in the sidebar. |
| 173 | $page_suffix = add_submenu_page( |
| 174 | '', |
| 175 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 176 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 177 | 'manage_options', |
| 178 | self::PAGE_SLUG, |
| 179 | $callback |
| 180 | ); |
| 181 | } else { |
| 182 | $page_suffix = Admin_Menu::add_menu( |
| 183 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 184 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 185 | 'manage_options', |
| 186 | self::PAGE_SLUG, |
| 187 | $callback, |
| 188 | 15 |
| 189 | ); |
| 190 | } |
| 191 | |
| 192 | if ( $page_suffix ) { |
| 193 | add_action( 'load-' . $page_suffix, array( __CLASS__, 'on_page_load' ) ); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Register the announcement page directly under the Jetpack menu. |
| 199 | * |
| 200 | * Used on WordPress.com (Simple and WoA), where jetpack-mu-wpcom's |
| 201 | * wpcom-admin-menu owns the Jetpack menu and registers submenus with the |
| 202 | * core add_submenu_page() at a late priority β not the standalone plugin's |
| 203 | * Admin_Menu wrapper. Mirrors Settings::add_wp_admin_submenu(). |
| 204 | * |
| 205 | * As in add_menu(), an empty parent slug keeps the page reachable at its URL |
| 206 | * (so the "remove from sidebar" choice can be undone) while hiding it from |
| 207 | * the sidebar. |
| 208 | * |
| 209 | * @return void |
| 210 | */ |
| 211 | public static function add_wp_admin_submenu() { |
| 212 | $callback = function_exists( 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' ) |
| 213 | ? 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' |
| 214 | : array( __CLASS__, 'render_fallback' ); |
| 215 | |
| 216 | $parent_slug = get_option( self::REMOVED_OPTION ) ? '' : 'jetpack'; |
| 217 | |
| 218 | $page_suffix = add_submenu_page( |
| 219 | $parent_slug, |
| 220 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 221 | __( 'Subscribers', 'jetpack-newsletter' ), |
| 222 | 'manage_options', |
| 223 | self::PAGE_SLUG, |
| 224 | $callback |
| 225 | ); |
| 226 | |
| 227 | if ( $page_suffix ) { |
| 228 | add_action( 'load-' . $page_suffix, array( __CLASS__, 'on_page_load' ) ); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Page-load actions: record the page view and expose the app data. |
| 234 | * |
| 235 | * @return void |
| 236 | */ |
| 237 | public static function on_page_load() { |
| 238 | add_action( 'admin_head', array( __CLASS__, 'print_app_data' ) ); |
| 239 | |
| 240 | self::tracking()->record_user_event( |
| 241 | 'subscribers_announcement_page_view', |
| 242 | array( 'menu_removed' => (bool) get_option( self::REMOVED_OPTION ) ) |
| 243 | ); |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Print the data the announcement app needs (URLs, nonce, current state). |
| 248 | * |
| 249 | * @return void |
| 250 | */ |
| 251 | public static function print_app_data() { |
| 252 | $data = array( |
| 253 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 254 | 'toggleAction' => self::TOGGLE_ACTION, |
| 255 | 'toggleNonce' => wp_create_nonce( self::TOGGLE_ACTION ), |
| 256 | // Built with add_query_arg (not wp_nonce_url, which HTML-escapes |
| 257 | // the ampersands) because the app navigates to it via JS. |
| 258 | 'goToNewsletterUrl' => add_query_arg( |
| 259 | array( |
| 260 | 'action' => self::GO_ACTION, |
| 261 | '_wpnonce' => wp_create_nonce( self::GO_ACTION ), |
| 262 | ), |
| 263 | admin_url( 'admin-post.php' ) |
| 264 | ), |
| 265 | 'menuRemoved' => (bool) get_option( self::REMOVED_OPTION ), |
| 266 | 'menuSlug' => self::PAGE_SLUG, |
| 267 | ); |
| 268 | |
| 269 | printf( |
| 270 | '<script>window.JetpackSubscribersAnnouncementData = %s;</script>', |
| 271 | wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) |
| 272 | ); |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * Minimal fallback when the wp-build bundle is unavailable. |
| 277 | * |
| 278 | * @return void |
| 279 | */ |
| 280 | public static function render_fallback() { |
| 281 | ?> |
| 282 | <div class="wrap"> |
| 283 | <h1><?php esc_html_e( 'Subscribers moved', 'jetpack-newsletter' ); ?></h1> |
| 284 | <p><?php esc_html_e( 'Now itβs part of Jetpack β Newsletter.', 'jetpack-newsletter' ); ?></p> |
| 285 | <p> |
| 286 | <a class="button button-primary" href="<?php echo esc_url( Urls::get_newsletter_settings_url() ); ?>"> |
| 287 | <?php esc_html_e( 'Take me to Newsletter', 'jetpack-newsletter' ); ?> |
| 288 | </a> |
| 289 | </p> |
| 290 | </div> |
| 291 | <?php |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * AJAX handler persisting the "remove Subscribers from the sidebar" choice. |
| 296 | * |
| 297 | * @return void |
| 298 | */ |
| 299 | public static function handle_toggle_menu() { |
| 300 | check_ajax_referer( self::TOGGLE_ACTION ); |
| 301 | |
| 302 | if ( ! current_user_can( 'manage_options' ) || ! self::is_enabled() ) { |
| 303 | wp_send_json_error( 'unauthorized', 403, JSON_HEX_TAG | JSON_HEX_AMP ); |
| 304 | } |
| 305 | |
| 306 | $removed = isset( $_POST['removed'] ) && '1' === $_POST['removed']; |
| 307 | update_option( self::REMOVED_OPTION, $removed ? 1 : 0, false ); |
| 308 | |
| 309 | self::tracking()->record_user_event( |
| 310 | 'subscribers_announcement_remove_menu_click', |
| 311 | array( 'removed' => $removed ) |
| 312 | ); |
| 313 | |
| 314 | wp_send_json_success( array( 'removed' => $removed ), 200, JSON_HEX_TAG | JSON_HEX_AMP ); |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * Admin-post handler recording the "Take me to Newsletter" click, then redirecting. |
| 319 | * |
| 320 | * Tracking the click server-side before the redirect avoids relying on a |
| 321 | * JS tracking pipeline on a page that is otherwise static. |
| 322 | * |
| 323 | * @return never |
| 324 | */ |
| 325 | public static function handle_go_to_newsletter() { |
| 326 | check_admin_referer( self::GO_ACTION ); |
| 327 | |
| 328 | if ( current_user_can( 'manage_options' ) && self::is_enabled() ) { |
| 329 | self::tracking()->record_user_event( 'subscribers_announcement_newsletter_click' ); |
| 330 | } |
| 331 | |
| 332 | wp_safe_redirect( admin_url( 'admin.php?page=' . Settings::ADMIN_PAGE_SLUG ) ); |
| 333 | exit( 0 ); |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Get a Tracking instance. |
| 338 | * |
| 339 | * The product name stays `jetpack` so the events are recorded as |
| 340 | * `jetpack_subscribers_announcement_*` regardless of which plugin |
| 341 | * bundles this package. |
| 342 | * |
| 343 | * @return Tracking |
| 344 | */ |
| 345 | private static function tracking() { |
| 346 | return new Tracking( 'jetpack', new Connection_Manager( 'jetpack' ) ); |
| 347 | } |
| 348 | |
| 349 | /** |
| 350 | * Returns true when the current request targets the announcement page. |
| 351 | * |
| 352 | * @return bool |
| 353 | */ |
| 354 | private static function is_announcement_request() { |
| 355 | if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 356 | return false; |
| 357 | } |
| 358 | |
| 359 | return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::PAGE_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 360 | } |
| 361 | } |