PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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
jetpack / jetpack_vendor / automattic / jetpack-my-jetpack / src / class-main-features.php

class-main-features.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-my-jetpack/src/class-main-features.php

993 lines 42.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Static catalog of the main Jetpack features.
4 *
5 * @package automattic/my-jetpack
6 */
7
8 namespace Automattic\Jetpack\My_Jetpack;
9
10 use Automattic\Jetpack\Plugins_Installer;
11
12 /**
13 * Describes the main Jetpack features: their copy, links and how a site owner gets each one.
14 *
15 * The descriptive fields may later come from a WordPress.com endpoint; what a feature is
16 * delivered by stays local, since only the site knows what is installed on it.
17 */
18 class Main_Features {
19
20 /**
21 * A plugin is not on the site at all.
22 */
23 const PLUGIN_NOT_INSTALLED = 'not-installed';
24
25 /**
26 * A plugin is installed and switched off.
27 */
28 const PLUGIN_INACTIVE = 'inactive';
29
30 /**
31 * A plugin is installed and running.
32 */
33 const PLUGIN_ACTIVE = 'active';
34
35 /**
36 * The current user may install plugins here.
37 */
38 const INSTALLS_ALLOWED = 'allowed';
39
40 /**
41 * Nobody may install plugins here: file changes are turned off for the whole site.
42 */
43 const INSTALLS_DISABLED = 'disabled';
44
45 /**
46 * Others may install plugins here, but not the current user.
47 */
48 const INSTALLS_NOT_PERMITTED = 'not_permitted';
49
50 /**
51 * The static feature catalog.
52 *
53 * Keyed by feature slug. `delivery` says what switches the feature: Protect and
54 * VaultPress Backup ship inside Jetpack too, but their switch is their own plugin, so
55 * they set `jetpack` false and name only that.
56 *
57 * @return array<string, array<string, mixed>> Feature definitions keyed by feature slug.
58 */
59 public static function get_feature_definitions() {
60 // Built once per locale: the REST route asks for it on every `rest_api_init` to
61 // bound its plugin argument, and rebuilding a hundred translated strings for that
62 // is a cost every request to the site pays, not just this page.
63 static $definitions = array();
64
65 $locale = get_user_locale();
66
67 if ( isset( $definitions[ $locale ] ) ) {
68 return $definitions[ $locale ];
69 }
70
71 $definitions[ $locale ] = array(
72 'activity-log' => array(
73 'info_url' => 'https://jetpack.com/security/activity-log/',
74 'docs_url' => 'https://jetpack.com/support/activity-log/',
75 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/activity-log.png',
76 'name' => __( 'Activity Log', 'jetpack-my-jetpack' ),
77 'description' => __( 'See what changed on your site, when it happened, and who did it.', 'jetpack-my-jetpack' ),
78 'long_description' => __( 'When something breaks, see exactly what changed and who changed it, from published posts to plugin updates and logins. Every connected site gets its 20 most recent events for free.', 'jetpack-my-jetpack' ),
79 'icon' => 'list',
80 'admin_page' => 'jetpack-activity-log',
81 'module' => 'activity-log',
82 'free_highlights' => array(
83 __( 'Your 20 most recent events', 'jetpack-my-jetpack' ),
84 __( 'Who changed what, from posts to plugin updates and logins', 'jetpack-my-jetpack' ),
85 ),
86 'paid_highlights' => array(
87 __( '30 days of history with VaultPress Backup or Jetpack Security, a full year with Jetpack Complete', 'jetpack-my-jetpack' ),
88 __( 'Filter events by activity type and date range', 'jetpack-my-jetpack' ),
89 ),
90 'delivery' => array(
91 'jetpack' => true,
92 'plugin' => '',
93 'free' => true,
94 ),
95 'plans' => array( 'security', 'complete' ),
96 // The WordPress.com site feature the Activity Log package gates its paid history on.
97 'paid_feature' => 'full-activity-log',
98 ),
99 'anti-spam' => array(
100 'info_url' => 'https://akismet.com/',
101 'docs_url' => 'https://akismet.com/support/',
102 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/anti-spam.png',
103 'name' => __( 'Akismet Anti-spam', 'jetpack-my-jetpack' ),
104 'description' => __( 'Stop comment and form spam without making visitors solve CAPTCHAs.', 'jetpack-my-jetpack' ),
105 'long_description' => __( 'Akismet filters spam out of your comments and form submissions in the background, so you stop moderating junk and visitors never have to prove they are human. When it gets one wrong, mark it and Akismet learns from your correction.', 'jetpack-my-jetpack' ),
106 'icon' => 'comment',
107 'product' => 'anti-spam',
108 'interstitial' => '/add-akismet',
109 'free_highlights' => array(
110 __( 'Comment and form spam filtering', 'jetpack-my-jetpack' ),
111 __( 'No CAPTCHAs for your visitors', 'jetpack-my-jetpack' ),
112 __( 'For personal, non-commercial sites', 'jetpack-my-jetpack' ),
113 ),
114 'paid_highlights' => array(
115 __( 'Use it on business and commercial sites', 'jetpack-my-jetpack' ),
116 __( 'Advanced stats and diagnostics for your spam activity', 'jetpack-my-jetpack' ),
117 __( 'Email support', 'jetpack-my-jetpack' ),
118 ),
119 'delivery' => array(
120 'jetpack' => false,
121 'plugin' => 'akismet',
122 'plugin_name' => __( 'Akismet Anti-spam', 'jetpack-my-jetpack' ),
123 'plugin_url' => 'https://wordpress.org/plugins/akismet/',
124 'free' => true,
125 ),
126 'plans' => array( 'security', 'complete' ),
127 'paid_product' => __( 'Jetpack Akismet Anti-spam', 'jetpack-my-jetpack' ),
128 ),
129 'backup' => array(
130 'info_url' => 'https://jetpack.com/backup/',
131 'docs_url' => 'https://jetpack.com/support/backup/',
132 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/backup.png',
133 'name' => __( 'VaultPress Backup', 'jetpack-my-jetpack' ),
134 'description' => __( 'Get your site back online in one click if anything goes wrong.', 'jetpack-my-jetpack' ),
135 'long_description' => __( 'Every change you make is saved off-site as it happens, so your content and WooCommerce orders stay safe even if your host goes down. Restore your site in one click, even when it is offline.', 'jetpack-my-jetpack' ),
136 'icon' => 'backup',
137 'product' => 'backup',
138 'interstitial' => '/add-backup',
139 'paid_highlights' => array(
140 __( 'Real-time backups of your files, database and WooCommerce orders', 'jetpack-my-jetpack' ),
141 __( 'One-click restores, even from the Jetpack mobile app', 'jetpack-my-jetpack' ),
142 __( 'Move your site to any host without a developer', 'jetpack-my-jetpack' ),
143 ),
144 'delivery' => array(
145 'jetpack' => false,
146 'plugin' => 'jetpack-backup',
147 'plugin_name' => __( 'Jetpack VaultPress Backup', 'jetpack-my-jetpack' ),
148 'plugin_url' => 'https://wordpress.org/plugins/jetpack-backup/',
149 'free' => false,
150 ),
151 'plans' => array( 'security', 'complete' ),
152 'paid_product' => __( 'Jetpack VaultPress Backup', 'jetpack-my-jetpack' ),
153 ),
154 'blaze' => array(
155 'info_url' => 'https://jetpack.com/blaze/',
156 'docs_url' => 'https://jetpack.com/support/blaze/',
157 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/blaze.png',
158 'name' => __( 'Blaze Ads', 'jetpack-my-jetpack' ),
159 'description' => __( 'Put your best posts in front of new readers on WordPress.com and Tumblr.', 'jetpack-my-jetpack' ),
160 'long_description' => __( 'Turn a post or page into an ad shown across Tumblr and WordPress.com, aimed at the locations, languages and interests you choose. There is no plan to buy: set a budget for each campaign and pay only for the ad views you get.', 'jetpack-my-jetpack' ),
161 'icon' => 'megaphone',
162 'admin_page' => 'advertising',
163 'module' => 'blaze',
164 'delivery' => array(
165 'jetpack' => true,
166 'plugin' => 'blaze-ads',
167 'plugin_name' => __( 'Blaze Ads', 'jetpack-my-jetpack' ),
168 'plugin_url' => 'https://wordpress.org/plugins/blaze-ads/',
169 'free' => false,
170 ),
171 ),
172 'boost' => array(
173 'info_url' => 'https://jetpack.com/boost/',
174 'docs_url' => 'https://jetpack.com/support/jetpack-boost/',
175 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/boost.png',
176 'name' => __( 'Boost', 'jetpack-my-jetpack' ),
177 'description' => __( 'Make your site load faster in a few clicks, no developer required.', 'jetpack-my-jetpack' ),
178 'long_description' => __( 'Your pages reach visitors sooner: Boost caches them, defers non-essential JavaScript and serves resized images from a worldwide network. Mobile and desktop performance scores show you the impact of each change.', 'jetpack-my-jetpack' ),
179 'icon' => 'trending-up',
180 'product' => 'boost',
181 'essential' => true,
182 'interstitial' => '/add-boost',
183 'free_highlights' => array(
184 __( 'Page caching and deferred JavaScript', 'jetpack-my-jetpack' ),
185 __( 'Resized images from a worldwide image CDN', 'jetpack-my-jetpack' ),
186 __( 'Mobile and desktop performance scores', 'jetpack-my-jetpack' ),
187 ),
188 'paid_highlights' => array(
189 __( 'Critical CSS that updates automatically as your site changes', 'jetpack-my-jetpack' ),
190 __( 'Performance history for mobile and desktop, including Core Web Vitals', 'jetpack-my-jetpack' ),
191 __( 'Control over the quality and size of images from the image CDN', 'jetpack-my-jetpack' ),
192 ),
193 'delivery' => array(
194 'jetpack' => false,
195 'plugin' => 'jetpack-boost',
196 'plugin_name' => __( 'Jetpack Boost', 'jetpack-my-jetpack' ),
197 'plugin_url' => 'https://wordpress.org/plugins/jetpack-boost/',
198 'free' => true,
199 ),
200 'plans' => array( 'complete' ),
201 'paid_product' => __( 'Jetpack Boost', 'jetpack-my-jetpack' ),
202 ),
203 'crm' => array(
204 'info_url' => 'https://jetpackcrm.com/',
205 'docs_url' => 'https://kb.jetpackcrm.com/article-categories/getting-started/',
206 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/crm.png',
207 'name' => __( 'Jetpack CRM', 'jetpack-my-jetpack' ),
208 'description' => __( 'Keep every lead and customer in one place, right inside WordPress.', 'jetpack-my-jetpack' ),
209 'long_description' => __( 'Track leads, send quotes and invoices, and see each customer’s full history from your dashboard. Because it runs on your own site, your customer data stays yours.', 'jetpack-my-jetpack' ),
210 'icon' => 'people',
211 'product' => 'crm',
212 'interstitial' => '/add-crm',
213 'free_highlights' => array(
214 __( 'Leads, contacts and each customer’s history', 'jetpack-my-jetpack' ),
215 __( 'Quotes and invoices', 'jetpack-my-jetpack' ),
216 __( 'Customer data that stays on your site', 'jetpack-my-jetpack' ),
217 ),
218 'paid_highlights' => array(
219 __( 'A sales dashboard, sales funnels and automations to follow up on leads', 'jetpack-my-jetpack' ),
220 __( 'Take payments through Stripe and PayPal', 'jetpack-my-jetpack' ),
221 __( 'Sync contacts from Gravity Forms, Contact Form 7 and Mailchimp', 'jetpack-my-jetpack' ),
222 ),
223 'delivery' => array(
224 'jetpack' => false,
225 'plugin' => 'zero-bs-crm',
226 'plugin_name' => __( 'Jetpack CRM', 'jetpack-my-jetpack' ),
227 'plugin_url' => 'https://wordpress.org/plugins/zero-bs-crm/',
228 'free' => true,
229 ),
230 'plans' => array( 'complete' ),
231 'paid_product' => __( 'Jetpack CRM Entrepreneur', 'jetpack-my-jetpack' ),
232 ),
233 'jetpack-ai' => array(
234 'info_url' => 'https://jetpack.com/ai/',
235 'docs_url' => 'https://jetpack.com/support/create-better-content-with-jetpack-ai/',
236 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/jetpack-ai-1.png',
237 'name' => __( 'Jetpack AI', 'jetpack-my-jetpack' ),
238 'description' => __( 'Turn your ideas into ready-to-publish content without leaving the editor.', 'jetpack-my-jetpack' ),
239 'long_description' => __( 'Generate posts, tables, forms and images from a prompt, then change the tone, translate, or fix unclear sentences to make your writing easier to read. Before you publish, improve your title and create a featured image in one click.', 'jetpack-my-jetpack' ),
240 'icon' => 'star',
241 'product' => 'jetpack-ai',
242 'interstitial' => '/add-jetpack-ai',
243 'free_highlights' => array(
244 /* translators: %d is the number of free requests, such as 20. */
245 sprintf( _n( '%d request to try it out', '%d requests to try it out', Products\Jetpack_Ai::FREE_REQUESTS, 'jetpack-my-jetpack' ), Products\Jetpack_Ai::FREE_REQUESTS ),
246 __( 'Generate, rewrite and translate in the editor', 'jetpack-my-jetpack' ),
247 ),
248 'paid_highlights' => array(
249 /* translators: %d is the number of free requests, such as 20. */
250 sprintf( _n( 'Keep creating after your %d free request runs out', 'Keep creating after your %d free requests run out', Products\Jetpack_Ai::FREE_REQUESTS, 'jetpack-my-jetpack' ), Products\Jetpack_Ai::FREE_REQUESTS ),
251 __( 'Generate a logo for your site', 'jetpack-my-jetpack' ),
252 __( 'Priority support', 'jetpack-my-jetpack' ),
253 ),
254 'delivery' => array(
255 'jetpack' => true,
256 'plugin' => '',
257 'free' => true,
258 ),
259 'plans' => array( 'complete' ),
260 'paid_product' => __( 'Jetpack AI Assistant', 'jetpack-my-jetpack' ),
261 ),
262 'jetpack-forms' => array(
263 'info_url' => 'https://jetpack.com/forms/',
264 'docs_url' => 'https://jetpack.com/support/jetpack-blocks/contact-form/',
265 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/jetpack-forms.png',
266 'name' => __( 'Forms', 'jetpack-my-jetpack' ),
267 'description' => __( 'From quick contact forms to multistep surveys, everything you need is included.', 'jetpack-my-jetpack' ),
268 'long_description' => __( 'Break long forms into steps with a progress bar, and get every response by email and in your dashboard, with no limit on responses. Upgrade to add secure file uploads that keep harmful files off your server.', 'jetpack-my-jetpack' ),
269 'icon' => 'list-bullets',
270 'product' => 'jetpack-forms',
271 'essential' => true,
272 'free_highlights' => array(
273 __( 'Unlimited responses, by email and in your dashboard', 'jetpack-my-jetpack' ),
274 __( 'Multi-step forms with a progress bar', 'jetpack-my-jetpack' ),
275 ),
276 'paid_highlights' => array(
277 __( 'Secure file uploads that keep harmful files off your server', 'jetpack-my-jetpack' ),
278 __( 'Visitors’ files stay private, visible only to you and your editors', 'jetpack-my-jetpack' ),
279 __( 'Up to 10 files per field, 20 MB each', 'jetpack-my-jetpack' ),
280 ),
281 'delivery' => array(
282 'jetpack' => true,
283 'plugin' => '',
284 'free' => true,
285 ),
286 'plans' => array( 'complete' ),
287 ),
288 'newsletter' => array(
289 'info_url' => 'https://jetpack.com/newsletter/',
290 'docs_url' => 'https://jetpack.com/support/newsletter/',
291 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/newsletter.png',
292 'name' => __( 'Newsletter', 'jetpack-my-jetpack' ),
293 'description' => __( 'Turn every post you publish into an email your readers receive.', 'jetpack-my-jetpack' ),
294 'long_description' => __( 'Subscribers get each new post by email as soon as you publish, or in a daily or weekly digest they choose. You can also earn from your writing with paid subscriptions for exclusive posts.', 'jetpack-my-jetpack' ),
295 'icon' => 'envelope',
296 'product' => 'newsletter',
297 'module' => 'subscriptions',
298 'free_highlights' => array(
299 __( 'Each new post emailed to your subscribers', 'jetpack-my-jetpack' ),
300 __( 'Daily or weekly digests your readers choose', 'jetpack-my-jetpack' ),
301 __( 'Paid subscriptions for exclusive posts', 'jetpack-my-jetpack' ),
302 ),
303 'paid_highlights' => array(
304 __( 'Keep more of what you earn with lower fees on paid subscriptions', 'jetpack-my-jetpack' ),
305 __( 'Import as many subscribers as you like', 'jetpack-my-jetpack' ),
306 ),
307 'delivery' => array(
308 'jetpack' => true,
309 'plugin' => '',
310 'free' => true,
311 ),
312 'plans' => array( 'growth', 'complete' ),
313 ),
314 'podcast' => array(
315 'info_url' => 'https://jetpack.com/podcast/',
316 'docs_url' => 'https://jetpack.com/support/jetpack-podcast/',
317 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/podcast.png',
318 'name' => __( 'Podcast', 'jetpack-my-jetpack' ),
319 'description' => __( 'Start a podcast on your own site and reach listeners on every major app.', 'jetpack-my-jetpack' ),
320 'long_description' => __( 'Publish unlimited episodes as posts, with your podcast feed served from your own domain. Submit your show to Apple Podcasts, Spotify, Pocket Casts and more from one place.', 'jetpack-my-jetpack' ),
321 'icon' => 'audio',
322 'admin_page' => 'jetpack-podcast',
323 'module' => 'podcast',
324 'free_highlights' => array(
325 __( 'Unlimited episodes, published as posts', 'jetpack-my-jetpack' ),
326 __( 'A podcast feed on your own domain', 'jetpack-my-jetpack' ),
327 __( 'Submit to Apple Podcasts, Spotify and more', 'jetpack-my-jetpack' ),
328 ),
329 'paid_highlights' => array(
330 __( 'Listener stats by episode, app and country', 'jetpack-my-jetpack' ),
331 __( 'An episodes dashboard for your whole back catalog', 'jetpack-my-jetpack' ),
332 __( 'An episode player with chapters, transcripts and soundbites', 'jetpack-my-jetpack' ),
333 ),
334 'delivery' => array(
335 'jetpack' => true,
336 'plugin' => '',
337 'free' => true,
338 ),
339 'plans' => array( 'growth', 'complete' ),
340 ),
341 'protect-dashboard' => array(
342 'info_url' => 'https://jetpack.com/protect/',
343 'docs_url' => 'https://jetpack.com/support/jetpack-protect/',
344 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/protect.png',
345 'name' => __( 'Protect', 'jetpack-my-jetpack' ),
346 'description' => __( 'Get warned about vulnerable plugins and stop login attacks on your site.', 'jetpack-my-jetpack' ),
347 'long_description' => __( 'Your WordPress version, plugins and themes are checked daily against a database of known vulnerabilities, with results in your dashboard. Login attacks are blocked automatically, and you can block specific IP addresses yourself.', 'jetpack-my-jetpack' ),
348 'icon' => 'shield',
349 'product' => 'protect',
350 // Not `protect`: that module is Brute Force Protection, which has its own row.
351 'module' => 'protect-dashboard',
352 'essential' => true,
353 'interstitial' => '/add-protect',
354 'free_highlights' => array(
355 __( 'Daily checks for known vulnerabilities', 'jetpack-my-jetpack' ),
356 __( 'Automatic blocking of login attacks', 'jetpack-my-jetpack' ),
357 __( 'Block IP addresses yourself', 'jetpack-my-jetpack' ),
358 ),
359 'paid_highlights' => array(
360 __( 'Daily malware scanning with one-click fixes for most threats', 'jetpack-my-jetpack' ),
361 __( 'Automatic firewall rules that block harmful requests', 'jetpack-my-jetpack' ),
362 __( 'Instant email alerts when a threat is found', 'jetpack-my-jetpack' ),
363 ),
364 'delivery' => array(
365 'jetpack' => false,
366 'plugin' => 'jetpack-protect',
367 'plugin_name' => __( 'Jetpack Protect', 'jetpack-my-jetpack' ),
368 'plugin_url' => 'https://wordpress.org/plugins/jetpack-protect/',
369 'free' => true,
370 ),
371 'plans' => array( 'security', 'complete' ),
372 'paid_product' => __( 'Jetpack Scan', 'jetpack-my-jetpack' ),
373 ),
374 'search' => array(
375 'info_url' => 'https://jetpack.com/search/',
376 'docs_url' => 'https://jetpack.com/support/search/',
377 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/search.png',
378 'name' => __( 'Search', 'jetpack-my-jetpack' ),
379 'description' => __( 'Help your visitors find the right post or product as they type.', 'jetpack-my-jetpack' ),
380 'long_description' => __( 'Visitors see relevant results as they type and can filter by category, tag, date or author, so they find what they need instead of leaving. Match search to your theme without writing code.', 'jetpack-my-jetpack' ),
381 'icon' => 'search',
382 'product' => 'search',
383 'interstitial' => '/add-search',
384 'free_highlights' => array(
385 __( 'Results as visitors type', 'jetpack-my-jetpack' ),
386 __( 'Filters for category, tag, date and author', 'jetpack-my-jetpack' ),
387 ),
388 'paid_highlights' => array(
389 __( 'Room for more records and monthly searches as your site grows', 'jetpack-my-jetpack' ),
390 __( 'AI Answers without the free plan’s limits', 'jetpack-my-jetpack' ),
391 __( 'No Jetpack branding, plus priority support', 'jetpack-my-jetpack' ),
392 ),
393 'delivery' => array(
394 'jetpack' => true,
395 'plugin' => 'jetpack-search',
396 'plugin_name' => __( 'Jetpack Search', 'jetpack-my-jetpack' ),
397 'plugin_url' => 'https://wordpress.org/plugins/jetpack-search/',
398 'free' => true,
399 ),
400 'plans' => array( 'complete' ),
401 'paid_product' => __( 'Jetpack Search', 'jetpack-my-jetpack' ),
402 ),
403 'social' => array(
404 'info_url' => 'https://jetpack.com/social/',
405 'docs_url' => 'https://jetpack.com/support/jetpack-social/',
406 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/social.png',
407 'name' => __( 'Social', 'jetpack-my-jetpack' ),
408 'description' => __( 'Share your posts to your social networks automatically when you publish.', 'jetpack-my-jetpack' ),
409 'long_description' => __( 'Reach your followers on Facebook, Instagram, Threads, Bluesky, LinkedIn, Mastodon, Nextdoor and Tumblr without leaving WordPress. Posts share automatically when you publish, and you can re-share or schedule them later.', 'jetpack-my-jetpack' ),
410 'icon' => 'share',
411 'product' => 'social',
412 'interstitial' => '/add-social',
413 'free_highlights' => array(
414 __( 'Automatic sharing when you publish', 'jetpack-my-jetpack' ),
415 __( 'Facebook, Instagram, Threads, Bluesky, LinkedIn and more', 'jetpack-my-jetpack' ),
416 ),
417 'paid_highlights' => array(
418 __( 'Tailor the message and media for each network', 'jetpack-my-jetpack' ),
419 __( 'Save reusable message templates for each account', 'jetpack-my-jetpack' ),
420 __( 'Branded share images from Social Image Generator', 'jetpack-my-jetpack' ),
421 ),
422 'delivery' => array(
423 'jetpack' => true,
424 'plugin' => 'jetpack-social',
425 'plugin_name' => __( 'Jetpack Social', 'jetpack-my-jetpack' ),
426 'plugin_url' => 'https://wordpress.org/plugins/jetpack-social/',
427 'free' => true,
428 ),
429 'plans' => array( 'growth', 'complete' ),
430 'paid_product' => __( 'Jetpack Social', 'jetpack-my-jetpack' ),
431 ),
432 'stats' => array(
433 'info_url' => 'https://jetpack.com/stats/',
434 'docs_url' => 'https://jetpack.com/support/jetpack-stats/',
435 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/stats.png',
436 'name' => __( 'Stats', 'jetpack-my-jetpack' ),
437 'description' => __( 'See how many people visit your site and which posts they read.', 'jetpack-my-jetpack' ),
438 'long_description' => __( 'Track views, visitors, top posts, referrers and visitor countries right from your WordPress dashboard. Learn what works without seeing who your individual visitors are.', 'jetpack-my-jetpack' ),
439 'icon' => 'chart-bar',
440 'product' => 'stats',
441 'essential' => true,
442 'interstitial' => '/add-stats',
443 'free_highlights' => array(
444 __( 'Views, visitors and top posts', 'jetpack-my-jetpack' ),
445 __( 'Referrers and visitor countries', 'jetpack-my-jetpack' ),
446 ),
447 'paid_highlights' => array(
448 __( 'UTM tracking to see which campaigns bring you traffic', 'jetpack-my-jetpack' ),
449 __( 'Device stats to see how visitors browse your site', 'jetpack-my-jetpack' ),
450 __( 'Region and city locations, beyond country level', 'jetpack-my-jetpack' ),
451 ),
452 'delivery' => array(
453 'jetpack' => true,
454 'plugin' => 'jetpack-stats',
455 'plugin_name' => __( 'Jetpack Stats', 'jetpack-my-jetpack' ),
456 'plugin_url' => 'https://wordpress.org/plugins/jetpack-stats/',
457 'free' => true,
458 ),
459 'plans' => array( 'growth', 'complete' ),
460 'paid_product' => __( 'Jetpack Stats', 'jetpack-my-jetpack' ),
461 ),
462 'videopress' => array(
463 'info_url' => 'https://jetpack.com/videopress/',
464 'docs_url' => 'https://jetpack.com/support/jetpack-videopress/',
465 'image' => 'https://jetpack.com/wp-content/uploads/2026/09/videopress-3.png',
466 'name' => __( 'VideoPress', 'jetpack-my-jetpack' ),
467 'description' => __( 'Share ad-free, high-quality video that keeps visitors on your site.', 'jetpack-my-jetpack' ),
468 'long_description' => __( 'Your videos play in an ad-free 4K player you can match to your brand, so visitors stay on your site. Drag and drop uploads right in the WordPress editor.', 'jetpack-my-jetpack' ),
469 'icon' => 'video',
470 'product' => 'videopress',
471 'interstitial' => '/add-videopress',
472 'free_highlights' => array(
473 __( 'One video in an ad-free 4K player', 'jetpack-my-jetpack' ),
474 __( 'A player you can match to your brand', 'jetpack-my-jetpack' ),
475 ),
476 'paid_highlights' => array(
477 __( 'Unlimited videos, instead of one', 'jetpack-my-jetpack' ),
478 __( 'Up to 1TB of video storage', 'jetpack-my-jetpack' ),
479 ),
480 'delivery' => array(
481 'jetpack' => true,
482 'plugin' => 'jetpack-videopress',
483 'plugin_name' => __( 'Jetpack VideoPress', 'jetpack-my-jetpack' ),
484 'plugin_url' => 'https://wordpress.org/plugins/jetpack-videopress/',
485 'free' => true,
486 ),
487 'plans' => array( 'complete' ),
488 'paid_product' => __( 'Jetpack VideoPress', 'jetpack-my-jetpack' ),
489 ),
490 );
491
492 return $definitions[ $locale ];
493 }
494
495 /**
496 * The paid bundles that include a feature, named for display.
497 *
498 * Named by the bundle products themselves, so the Features tab and the Products tab
499 * call a plan the same thing.
500 *
501 * @param array $definition One feature's catalog entry.
502 * @return array List of slug/name pairs.
503 */
504 private static function get_plan_badges( $definition ) {
505 $badges = array();
506
507 foreach ( $definition['plans'] ?? array() as $slug ) {
508 $bundle_class = Products::get_product_class( $slug );
509
510 if ( $bundle_class ) {
511 $badges[] = array(
512 'slug' => $slug,
513 'name' => $bundle_class::get_title(),
514 );
515 }
516 }
517
518 return $badges;
519 }
520
521 /**
522 * Where the modal's Upgrade button goes: the feature's own product page, else the cheapest bundle that includes it.
523 *
524 * @param array $definition A single entry from the feature catalog.
525 * @param string|null $product_class The product behind the feature, when it has one.
526 * @return array{path: string, name: string} The My Jetpack route and the product it sells, both empty when nothing does.
527 */
528 private static function get_upgrade( $definition, $product_class ) {
529 $none = array(
530 'path' => '',
531 'name' => '',
532 );
533
534 // Nothing to sell a site that already pays for the feature, directly or through a bundle.
535 if ( $product_class && self::pays_for_product( $product_class ) ) {
536 return $none;
537 }
538
539 foreach ( $definition['plans'] ?? array() as $plan ) {
540 $bundle_class = Products::get_product_class( $plan );
541
542 if ( $bundle_class && $bundle_class::has_paid_plan_for_product() ) {
543 return $none;
544 }
545 }
546
547 // Covers plans no bundle class lists, such as the legacy Jetpack Personal, Premium and Professional.
548 if ( ! empty( $definition['paid_feature'] ) && Product::does_site_have_feature( $definition['paid_feature'] ) ) {
549 return $none;
550 }
551
552 if ( ! empty( $definition['interstitial'] ) ) {
553 return array(
554 'path' => $definition['interstitial'],
555 'name' => $definition['paid_product'] ?? '',
556 );
557 }
558
559 // Plans list the cheapest bundle first, so this sells the least a site needs to buy.
560 foreach ( $definition['plans'] ?? array() as $plan ) {
561 $bundle_class = Products::get_product_class( $plan );
562
563 if ( $bundle_class ) {
564 return array(
565 'path' => '/add-' . $plan,
566 'name' => $bundle_class::get_title(),
567 );
568 }
569 }
570
571 return $none;
572 }
573
574 /**
575 * Whether the site pays for a product, not counting the product's own free plan.
576 *
577 * @param string $product_class The product behind the feature.
578 * @return bool
579 */
580 private static function pays_for_product( $product_class ) {
581 if ( ! $product_class::has_paid_plan_for_product() ) {
582 return false;
583 }
584
585 $free_slug = $product_class::get_wpcom_free_product_slug();
586 $purchases = Wpcom_Products::get_site_current_purchases();
587 $purchases = is_array( $purchases ) ? $purchases : array();
588 $paid = $free_slug
589 ? array_filter( $purchases, fn( $purchase ) => false === strpos( $purchase->product_slug, $free_slug ) )
590 : $purchases;
591
592 if ( count( $paid ) === count( $purchases ) ) {
593 return true;
594 }
595
596 /*
597 * A free plan such as `jetpack_search_free` matches the paid slug by substring and gets the
598 * same site feature, so only a purchase besides it counts.
599 */
600 $paid_slugs = array_merge(
601 $product_class::get_paid_bundles_that_include_product(),
602 $product_class::get_paid_plan_product_slugs()
603 );
604
605 foreach ( $paid as $purchase ) {
606 foreach ( $paid_slugs as $paid_slug ) {
607 if ( false !== strpos( $purchase->product_slug, $paid_slug ) ) {
608 return true;
609 }
610 }
611 }
612
613 return false;
614 }
615
616 /**
617 * The plugins the Features tab may install or switch: every standalone plugin in the map,
618 * and Jetpack itself.
619 *
620 * @return string[] WordPress.org plugin slugs.
621 */
622 public static function get_switchable_plugins() {
623 $plugins = array_filter( array_column( array_column( self::get_feature_definitions(), 'delivery' ), 'plugin' ) );
624
625 return array_values( array_unique( array_merge( array( Product::JETPACK_PLUGIN_SLUG ), $plugins ) ) );
626 }
627
628 /**
629 * Whether a plugin is missing, installed and off, or running.
630 *
631 * @param string $slug WordPress.org plugin slug.
632 * @param string|null $product_class The product behind the plugin, when it has one.
633 * @return string One of the PLUGIN_* constants.
634 */
635 public static function get_plugin_status( $slug, $product_class = null ) {
636 $file = self::get_plugin_file( $slug, $product_class );
637
638 if ( ! $file ) {
639 return self::PLUGIN_NOT_INSTALLED;
640 }
641
642 // A network-activated plugin is running here, whatever this site's own list says.
643 return 'inactive' === Plugins_Installer::get_plugin_status( $file )
644 ? self::PLUGIN_INACTIVE
645 : self::PLUGIN_ACTIVE;
646 }
647
648 /**
649 * Whether a plugin is forced on or off where this site's own switch can't change it.
650 *
651 * Mirrors `Jetpack_Modules_Overrides`: an `option_active_plugins` filter that adds or
652 * drops the plugin whatever the stored list says, or a network activation.
653 *
654 * @since 6.6.0
655 *
656 * @param string $slug WordPress.org plugin slug.
657 * @param string|null $product_class The product behind the plugin, when it has one.
658 * @return string 'active' or 'inactive' when forced, or an empty string when the owner decides.
659 */
660 public static function get_plugin_override( $slug, $product_class = null ) {
661 $file = self::get_plugin_file( $slug, $product_class );
662
663 if ( ! $file ) {
664 return '';
665 }
666
667 if ( 'network-active' === Plugins_Installer::get_plugin_status( $file ) ) {
668 return self::PLUGIN_ACTIVE;
669 }
670
671 if ( ! has_filter( 'option_active_plugins' ) ) {
672 return '';
673 }
674
675 /** This filter is documented in wp-includes/option.php */
676 if ( in_array( $file, (array) apply_filters( 'option_active_plugins', array(), 'active_plugins' ), true ) ) {
677 return self::PLUGIN_ACTIVE;
678 }
679
680 /** This filter is documented in wp-includes/option.php */
681 if ( ! in_array( $file, (array) apply_filters( 'option_active_plugins', array( $file ), 'active_plugins' ), true ) ) {
682 return self::PLUGIN_INACTIVE;
683 }
684
685 return '';
686 }
687
688 /**
689 * The installed file for a plugin, by the names its product declares where there is one.
690 *
691 * A product lists every folder its plugin ships under, including the -dev checkout.
692 *
693 * @param string $slug WordPress.org plugin slug.
694 * @param string|null $product_class The product behind the plugin, when it has one.
695 * @return string|false The plugin file, or false when it is not installed.
696 */
697 public static function get_plugin_file( $slug, $product_class = null ) {
698 if ( Product::JETPACK_PLUGIN_SLUG === $slug ) {
699 return Product::get_installed_plugin_filename( 'jetpack' ) ?? false;
700 }
701
702 if ( $product_class && $product_class::$has_standalone_plugin ) {
703 return $product_class::get_installed_plugin_filename();
704 }
705
706 return Plugins_Installer::get_plugin_id_by_slug( $slug );
707 }
708
709 /**
710 * The plugin this copy of My Jetpack is running from.
711 *
712 * My Jetpack ships inside Jetpack, Boost, Protect, Social, Search and VideoPress, and
713 * whichever one wins the autoloader renders this page — so that is the plugin a switch
714 * here must never turn off.
715 *
716 * @return string The plugin's folder name, or an empty string when it is not under one.
717 */
718 public static function get_hosting_plugin_slug() {
719 return self::plugin_slug_from_path( WP_PLUGIN_DIR, __DIR__ );
720 }
721
722 /**
723 * Whether a plugin slug names the plugin this copy of My Jetpack runs from.
724 *
725 * Compared by folder rather than by slug: a checkout in `jetpack-protect-dev` serves
726 * the page just as well, and never equals the `jetpack-protect` a request carries.
727 *
728 * @param string $slug WordPress.org plugin slug.
729 * @return bool True when the plugin is the one this page is being served from.
730 */
731 public static function is_hosting_plugin( $slug ) {
732 $host = self::get_hosting_plugin_slug();
733
734 if ( '' === $host ) {
735 return false;
736 }
737
738 $file = self::get_plugin_file( $slug, self::get_product_class_for_plugin( $slug ) );
739
740 return $file ? dirname( (string) $file ) === $host : $host === $slug;
741 }
742
743 /**
744 * The plugin folder a path sits in.
745 *
746 * @param string $plugins_dir The plugin directory, as WP_PLUGIN_DIR gives it.
747 * @param string $path The path to place.
748 * @return string The plugin's folder name, or an empty string when the path is outside.
749 */
750 public static function plugin_slug_from_path( $plugins_dir, $path ) {
751 $plugins_dir = wp_normalize_path( trailingslashit( $plugins_dir ) );
752 $path = wp_normalize_path( $path );
753
754 if ( ! str_starts_with( $path, $plugins_dir ) ) {
755 return '';
756 }
757
758 $segments = explode( '/', substr( $path, strlen( $plugins_dir ) ) );
759
760 return $segments[0];
761 }
762
763 /**
764 * Whether this plugin is the only active one carrying My Jetpack.
765 *
766 * Several plugins bundle it and the autoloader picks one, so the plugin serving this
767 * page is often not the only one that could. Switching that one off is only a problem
768 * when nothing else is left to take over.
769 *
770 * @param string $slug The plugin's folder name.
771 * @return bool True when no other active plugin carries My Jetpack.
772 */
773 public static function is_only_my_jetpack_provider( $slug ) {
774 $active = array_merge(
775 (array) get_option( 'active_plugins', array() ),
776 array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) )
777 );
778
779 foreach ( $active as $file ) {
780 $folder = dirname( (string) $file );
781
782 if ( '.' === $folder || $folder === $slug ) {
783 continue;
784 }
785
786 if ( is_dir( WP_PLUGIN_DIR . '/' . $folder . '/jetpack_vendor/automattic/jetpack-my-jetpack' ) ) {
787 return false;
788 }
789 }
790
791 return true;
792 }
793
794 /**
795 * The product class behind a plugin in the map, for surfaces holding only its slug.
796 *
797 * @param string $slug WordPress.org plugin slug.
798 * @return string|null The product class, or null when no product ships that plugin.
799 */
800 public static function get_product_class_for_plugin( $slug ) {
801 foreach ( self::get_feature_definitions() as $definition ) {
802 if ( ( $definition['delivery']['plugin'] ?? '' ) !== $slug || ! isset( $definition['product'] ) ) {
803 continue;
804 }
805
806 $product_class = Products::get_product_class( $definition['product'] );
807
808 if ( $product_class && $product_class::$has_standalone_plugin ) {
809 return $product_class;
810 }
811 }
812
813 return null;
814 }
815
816 /**
817 * Headings for the modules the feature list does not cover.
818 *
819 * Grouped by the job a site owner is doing, not by Jetpack's module tags, which describe
820 * mechanism (the Image CDN is tagged Appearance). Anything unlisted falls into Other.
821 *
822 * @return array Groups, each with a label and its module slugs. The UI sorts both by name.
823 */
824 public static function get_module_groups() {
825 return array(
826 array(
827 'label' => __( 'Security', 'jetpack-my-jetpack' ),
828 'modules' => array( 'account-protection', 'monitor', 'protect', 'sso', 'waf' ),
829 ),
830 array(
831 'label' => __( 'Performance', 'jetpack-my-jetpack' ),
832 'modules' => array( 'photon', 'photon-cdn' ),
833 ),
834 array(
835 'label' => __( 'Search engines', 'jetpack-my-jetpack' ),
836 'modules' => array( 'sitemaps', 'seo-tools', 'canonical-urls', 'verification-tools' ),
837 ),
838 array(
839 'label' => __( 'Engagement', 'jetpack-my-jetpack' ),
840 'modules' => array(
841 'comments',
842 'likes',
843 'comment-likes',
844 'gravatar-hovercards',
845 'related-posts',
846 'sharedaddy',
847 'shortlinks',
848 ),
849 ),
850 array(
851 'label' => __( 'Writing', 'jetpack-my-jetpack' ),
852 'modules' => array(
853 'blocks',
854 'markdown',
855 'latex',
856 'shortcodes',
857 'copy-post',
858 'custom-content-types',
859 'post-by-email',
860 'post-list',
861 'carousel',
862 'tiled-gallery',
863 ),
864 ),
865 array(
866 'label' => __( 'Design', 'jetpack-my-jetpack' ),
867 'modules' => array( 'google-fonts', 'widgets', 'widget-visibility' ),
868 ),
869 array(
870 'label' => __( 'Earn', 'jetpack-my-jetpack' ),
871 'modules' => array( 'wordads' ),
872 ),
873 array(
874 'label' => __( 'Analytics', 'jetpack-my-jetpack' ),
875 'modules' => array( 'woocommerce-analytics' ),
876 ),
877 );
878 }
879
880 /**
881 * Everything the Features tab renders from: the Jetpack plugin's status, each feature, the
882 * headings for Jetpack's other modules, and whether the current user may install plugins.
883 *
884 * @return array{jetpack: string, features: array, module_groups: array, plugin_installs: string} The state.
885 */
886 public static function get_state() {
887 return array(
888 'jetpack' => self::get_plugin_status( Product::JETPACK_PLUGIN_SLUG ),
889 'features' => self::get_features(),
890 'module_groups' => self::get_module_groups(),
891 'plugin_installs' => self::get_install_access(),
892 );
893 }
894
895 /**
896 * Whether the current user may install plugins here, and if not, why not.
897 *
898 * @since 6.7.0
899 *
900 * @return string One of the INSTALLS_* constants.
901 */
902 public static function get_install_access() {
903 if ( current_user_can( 'install_plugins' ) ) {
904 return self::INSTALLS_ALLOWED;
905 }
906
907 // Core's own map_meta_cap() check for install_plugins: DISALLOW_FILE_MODS or the file_mod_allowed filter.
908 if ( ! wp_is_file_mod_allowed( 'capability_update_core' ) ) {
909 return self::INSTALLS_DISABLED;
910 }
911
912 return self::INSTALLS_NOT_PERMITTED;
913 }
914
915 /**
916 * The feature catalog merged with each feature's live state, sorted by name.
917 *
918 * @return array List of features, each with slug, name, description, icon, manage_url,
919 * the plugin it ships as and the product/module join keys.
920 */
921 public static function get_features() {
922 $features = array();
923 $hidden = Feature_Visibility::get_hidden();
924
925 foreach ( self::get_feature_definitions() as $slug => $definition ) {
926 // A host can name the feature by its own slug or by its product's or module's.
927 $names = array_filter( array( $slug, $definition['product'] ?? '', $definition['module'] ?? '' ) );
928 if ( array_intersect( $names, $hidden ) ) {
929 continue;
930 }
931
932 $delivery = $definition['delivery'] ?? array();
933 $plugin = $delivery['plugin'] ?? '';
934 $product_class = isset( $definition['product'] ) ? Products::get_product_class( $definition['product'] ) : null;
935
936 $features[] = array(
937 'slug' => $slug,
938 'name' => $definition['name'],
939 'description' => $definition['description'],
940 'long_description' => $definition['long_description'] ?? '',
941 'icon' => $definition['icon'],
942 'manage_url' => self::get_feature_manage_url( $definition, $product_class ),
943 'essential' => ! empty( $definition['essential'] ),
944 'in_jetpack' => $delivery['jetpack'] ?? false,
945 'plugin' => $plugin,
946 'plugin_name' => $plugin ? ( $delivery['plugin_name'] ?? $definition['name'] ) : '',
947 'plugin_url' => $plugin ? ( $delivery['plugin_url'] ?? '' ) : '',
948 'plugin_status' => $plugin ? self::get_plugin_status( $plugin, $product_class ) : self::PLUGIN_NOT_INSTALLED,
949 'plugin_override' => $plugin ? self::get_plugin_override( $plugin, $product_class ) : '',
950 'free_highlights' => $definition['free_highlights'] ?? array(),
951 'paid_highlights' => $definition['paid_highlights'] ?? array(),
952 'upgrade' => self::get_upgrade( $definition, $product_class ),
953 'screenshot' => $definition['image'],
954 'plans' => self::get_plan_badges( $definition ),
955 'info_url' => $definition['info_url'],
956 'docs_url' => $definition['docs_url'],
957 // Join keys: the UI reads live state from the module and plugin it names,
958 // so the catalog never ships a status of its own.
959 'product' => $definition['product'] ?? '',
960 'module' => $definition['module'] ?? '',
961 );
962 }
963
964 usort(
965 $features,
966 function ( $a, $b ) {
967 return strnatcasecmp( $a['name'], $b['name'] );
968 }
969 );
970
971 return $features;
972 }
973
974 /**
975 * Where an active feature lives.
976 *
977 * @param array $definition A single entry from the feature catalog.
978 * @param string|null $product_class The product behind the feature, when it has one.
979 * @return string Admin URL, or an empty string when the feature has nowhere to go.
980 */
981 private static function get_feature_manage_url( array $definition, $product_class = null ) {
982 if ( isset( $definition['admin_page'] ) ) {
983 return admin_url( 'admin.php?page=' . $definition['admin_page'] );
984 }
985
986 if ( $product_class ) {
987 return (string) $product_class::get_manage_url();
988 }
989
990 return '';
991 }
992 }
993