Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.50% covered (success)
92.50%
74 / 80
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_My_Jetpack
96.10% covered (success)
96.10%
74 / 77
90.00% covered (success)
90.00%
9 / 10
24
0.00% covered (danger)
0.00%
0 / 1
 should_register
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_page_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_menu_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_initial_state
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_tools
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 get_tool_url
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 get_tool_key
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 get_tool_title
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_known_tools
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
3
 filter_modules_management_available
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- Feature entry file, named after the feature, also holds the WPCOM_My_Jetpack bootstrap class.
2/**
3 * A lightweight My Jetpack page for WordPress.com Simple sites.
4 *
5 * It lists what the site's Jetpack menu already holds, and steps aside wherever the real My Jetpack
6 * registers. It is a stopgap: delete it once My Jetpack itself runs on Simple.
7 *
8 * @package automattic/jetpack-mu-wpcom
9 */
10
11namespace Automattic\Jetpack\Jetpack_Mu_Wpcom;
12
13use Automattic\Jetpack\Admin_UI\Admin_Menu;
14use Automattic\Jetpack\Status\Host;
15
16require_once __DIR__ . '/../../common/class-wpcom-wp-build-page.php';
17
18/**
19 * Registers the page and its wp-build assets, unless the real My Jetpack claimed the slug.
20 */
21class WPCOM_My_Jetpack extends WPCOM_WP_Build_Page {
22
23    /**
24     * Where the rest of Jetpack links My Jetpack, and first in the Jetpack menu's sort.
25     */
26    const MENU_SLUG = 'my-jetpack';
27
28    /**
29     * As declared in routes/wpcom-my-jetpack/package.json.
30     */
31    const WP_BUILD_PAGE = 'wpcom-my-jetpack';
32
33    /**
34     * Authors and up: Contributors can reach no Jetpack tool on Simple, so the page would only ever be empty for them.
35     */
36    const CAPABILITY = 'publish_posts';
37
38    /**
39     * Global the page's boot script reads get_initial_state() from.
40     */
41    const INITIAL_STATE_VAR = 'wpcomMyJetpackInitialState';
42
43    /**
44     * Records clicks, to see which tools people reach from here.
45     */
46    const ENQUEUE_TRACKS = true;
47
48    /**
49     * Whether this page should claim its slug: everywhere but P2s, which drop the Jetpack menu.
50     *
51     * @return bool
52     */
53    public static function should_register() {
54        return ! ( new Host() )->is_p2_site();
55    }
56
57    /**
58     * The page title.
59     *
60     * @return string
61     */
62    public static function get_page_title() {
63        return __( 'My Jetpack', 'jetpack-mu-wpcom' );
64    }
65
66    /**
67     * The menu entry's label.
68     *
69     * @return string
70     */
71    public static function get_menu_title() {
72        return __( 'My Jetpack', 'jetpack-mu-wpcom' );
73    }
74
75    /**
76     * The state the page's boot script reads.
77     *
78     * @return array
79     */
80    public static function get_initial_state() {
81        return array(
82            'tools' => static::get_tools(),
83        );
84    }
85
86    /**
87     * Gets the tools to list: the visible Jetpack menu items, in menu order.
88     *
89     * @return array[] Tools with `title`, `description`, `key`, `section` and `url` keys.
90     */
91    public static function get_tools() {
92        global $submenu;
93
94        // This runs at enqueue, before admin_head drops hidden items, so an entry stays off this page
95        // only if admin-ui hides it or it carries `hide-if-js`.
96        if ( method_exists( Admin_Menu::class, 'remove_hidden_menu_items' ) ) {
97            Admin_Menu::remove_hidden_menu_items();
98        }
99
100        $known = static::get_known_tools();
101        $tools = array();
102
103        foreach ( $submenu[ static::PARENT_SLUG ] ?? array() as $item ) {
104            if ( static::MENU_SLUG === $item[2] || str_contains( $item[4] ?? '', 'hide-if-js' ) ) {
105                continue;
106            }
107
108            // Slugs and titles are escaped for wp-admin's menu HTML, and React escapes them again.
109            $slug = html_entity_decode( $item[2], ENT_QUOTES, 'UTF-8' );
110
111            // Drops a link in any protocol WordPress doesn't allow, like `javascript:`.
112            $url = esc_url_raw( static::get_tool_url( $slug ) );
113            if ( '' === $url || isset( $tools[ $url ] ) ) {
114                continue;
115            }
116
117            $key  = static::get_tool_key( $slug );
118            $tool = $known[ $key ] ?? null;
119
120            $tools[ $url ] = array(
121                'title'       => static::get_tool_title( $item[0] ),
122                'description' => $tool['description'] ?? '',
123                'key'         => $key,
124                'section'     => $tool['section'] ?? 'more',
125                'url'         => $url,
126            );
127        }
128
129        return array_values( $tools );
130    }
131
132    /**
133     * Gets where a menu slug leads, the way wp-admin's sidebar links it.
134     *
135     * @param string $slug The menu slug.
136     * @return string
137     */
138    private static function get_tool_url( $slug ) {
139        if ( wp_parse_url( $slug, PHP_URL_HOST ) ) {
140            return $slug;
141        }
142
143        // Only a slug with a page callback goes through admin.php; any other is a path under wp-admin.
144        if ( get_plugin_page_hook( $slug, static::PARENT_SLUG ) ) {
145            return add_query_arg( 'page', $slug, admin_url( 'admin.php' ) );
146        }
147
148        return admin_url( $slug );
149    }
150
151    /**
152     * Gets the key a menu slug is known by: the path of a WordPress.com link to this site, or the slug without `jetpack-`.
153     *
154     * @param string $slug The menu slug.
155     * @return string The key, or '' for any other link that leaves wp-admin, so no address reaches Tracks.
156     */
157    private static function get_tool_key( $slug ) {
158        $domain = wp_parse_url( home_url(), PHP_URL_HOST );
159
160        if ( preg_match( '#^https://wordpress\.com/(.+)/' . preg_quote( $domain, '#' ) . '/?$#', $slug, $matches ) ) {
161            $key = $matches[1];
162        } elseif ( wp_parse_url( $slug, PHP_URL_HOST ) ) {
163            return '';
164        } else {
165            $key = preg_replace( '/^jetpack-/', '', $slug );
166        }
167
168        $aliases = array(
169            'forms-admin'              => 'forms',
170            'forms-responses-wp-admin' => 'forms',
171        );
172
173        return $aliases[ $key ] ?? $key;
174    }
175
176    /**
177     * Gets a menu title's text, without the count badges rendered into its spans.
178     *
179     * @param string $title The menu title's HTML.
180     * @return string
181     */
182    private static function get_tool_title( $title ) {
183        $text = trim( wp_strip_all_tags( preg_replace( '#<span\b[^>]*>.*?</span>#s', '', $title ) ) );
184
185        // A title may wrap its whole name in a span.
186        return html_entity_decode( '' !== $text ? $text : wp_strip_all_tags( $title ), ENT_QUOTES, 'UTF-8' );
187    }
188
189    /**
190     * Gets each known tool's section and description, keyed as get_tool_key() returns.
191     *
192     * The key also names the tool's icon in the page's script. A tool missing here shows under More, with the Jetpack mark.
193     *
194     * @return array[] Tools with `section` and `description` keys.
195     */
196    private static function get_known_tools() {
197        $tools = array(
198            'security' => array(
199                'activity-log'       => __( 'See every change made to this site', 'jetpack-mu-wpcom' ),
200                'akismet-key-config' => __( 'Filter spam from comments and forms', 'jetpack-mu-wpcom' ),
201                'backup'             => __( 'Restorable copies of your site', 'jetpack-mu-wpcom' ),
202                'protect'            => __( 'Protect your site from malware and attacks', 'jetpack-mu-wpcom' ),
203                'scan'               => __( 'Check your site for malware and threats', 'jetpack-mu-wpcom' ),
204            ),
205            'growth'   => array(
206                'advertising'       => __( 'Promote posts across WordPress.com and Tumblr', 'jetpack-mu-wpcom' ),
207                'earn'              => __( 'Earn from ads, donations and paid content', 'jetpack-mu-wpcom' ),
208                'marketing/traffic' => __( 'Analytics and site verification', 'jetpack-mu-wpcom' ),
209                'newsletter'        => __( 'Send your posts to subscribers', 'jetpack-mu-wpcom' ),
210                'seo'               => __( 'Titles and descriptions for search engines', 'jetpack-mu-wpcom' ),
211                'social'            => __( 'Share posts to your social accounts', 'jetpack-mu-wpcom' ),
212            ),
213            'content'  => array(
214                'forms'      => __( 'Collect and review form responses', 'jetpack-mu-wpcom' ),
215                'podcast'    => __( 'Publish your posts as a podcast feed', 'jetpack-mu-wpcom' ),
216                'search'     => __( 'Fast, relevant search for your visitors', 'jetpack-mu-wpcom' ),
217                'videopress' => __( 'Ad-free video hosting', 'jetpack-mu-wpcom' ),
218            ),
219        );
220
221        $known = array();
222        foreach ( $tools as $section => $descriptions ) {
223            foreach ( $descriptions as $key => $description ) {
224                $known[ $key ] = array(
225                    'section'     => $section,
226                    'description' => $description,
227                );
228            }
229        }
230
231        return $known;
232    }
233
234    /**
235     * Tells code that links to My Jetpack's Features screen that this page doesn't have one.
236     *
237     * @param bool $available Whether modules can be managed from My Jetpack.
238     * @return bool
239     */
240    public static function filter_modules_management_available( $available ) {
241        return $available && ! static::owns_page();
242    }
243}
244
245add_action( 'init', array( WPCOM_My_Jetpack::class, 'init' ) );
246add_filter( 'jetpack_my_jetpack_modules_management_available', array( WPCOM_My_Jetpack::class, 'filter_modules_management_available' ) );