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