PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.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 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-seo / src / class-initializer.php

class-initializer.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.3, at jetpack_vendor/automattic/jetpack-seo/src/class-initializer.php

367 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Jetpack SEO — the visibility command center for WordPress sites.
4 *
5 * Gates the surface behind its feature flag and cohort, then wires the admin
6 * page ({@see Admin_Page}), the dashboard's REST reads ({@see Dashboard_Data}),
7 * the content-coverage cache invalidation ({@see Content_Coverage}), and the
8 * opt-in surface ({@see Surface_Visibility}).
9 *
10 * @package automattic/jetpack-seo-package
11 */
12
13 namespace Automattic\Jetpack\SEO;
14
15 use Automattic\Jetpack\Current_Plan;
16 use Automattic\Jetpack\Modules;
17 use Automattic\Jetpack\Status;
18 use Automattic\Jetpack\Status\Host;
19
20 /**
21 * Boots the package and carries its cross-plugin contract: the feature flag,
22 * the script-data key, and the option names / visibility reads other plugins consume.
23 */
24 class Initializer {
25
26 /**
27 * Jetpack SEO package version.
28 *
29 * @var string
30 */
31 const PACKAGE_VERSION = '0.9.5';
32
33 /**
34 * WordPress.com site feature that enables the Jetpack SEO surface.
35 *
36 * Kept separate from `advanced-seo`, which gates the paid parts of the
37 * dashboard after this product-level availability check has passed.
38 *
39 * @var string
40 */
41 const FEATURE_SLUG = 'seo-admin-ui';
42
43 /**
44 * Filter name that can enable the entire Jetpack SEO surface.
45 *
46 * The surface is available when this filter returns true or the current site's
47 * active features include {@see self::FEATURE_SLUG}. When neither is enabled,
48 * the package registers no admin menu or assets and changes nothing about the
49 * existing Jetpack UI.
50 *
51 * @var string
52 */
53 const FEATURE_FILTER = 'rsm_jetpack_seo';
54
55 /**
56 * Key under `window.JetpackScriptData` the React app reads its state from
57 * (`window.JetpackScriptData.seo`). Must match the JS-side reader in
58 * `_inc/data/get-overview.ts`.
59 */
60 const SCRIPT_DATA_KEY = 'seo';
61
62 /**
63 * Option recording whether sitemap generation is enabled.
64 *
65 * Read in place of the standalone `sitemaps` module's active state. Module-active
66 * state is filtered against the modules present on disk, so once that module is
67 * removed it would read as inactive even for sites that had it on. A one-time
68 * migration in the Jetpack plugin seeds this option from the site's existing module
69 * state and keeps it in sync while the legacy module still exists. See
70 * `Jetpack::migrate_sitemaps_module_to_seo_option()`.
71 *
72 * @var string
73 */
74 const SITEMAP_ENABLED_OPTION = 'jetpack_seo_sitemap_enabled';
75
76 /**
77 * Option recording that the user has deliberately turned the site's sitemap OFF,
78 * so WordPress core's own sitemap should be suppressed too ("off" means no sitemap
79 * at all, not a fallback to `/wp-sitemap.xml`).
80 *
81 * Set when the sitemaps module is switched off and cleared when it's switched on
82 * (see {@see self::flag_sitemap_user_disabled()} / {@see self::clear_sitemap_user_disabled()}),
83 * so it captures a deliberate off — a *transition* — rather than the ambient
84 * off-state. A site that simply never enabled the sitemap never fires the toggle,
85 * so the flag stays absent and its existing (e.g. WordPress-native) sitemap is left
86 * untouched.
87 *
88 * @var string
89 */
90 const SUPPRESS_WP_SITEMAP_OPTION = 'jetpack_seo_suppress_wp_sitemap';
91
92 /**
93 * Option recording whether canonical URLs are enabled.
94 *
95 * Read in place of the standalone `canonical-urls` module's active state. Module-active
96 * state is filtered against the modules present on disk, so once that module is
97 * removed it would read as inactive even for sites that had it on. A one-time
98 * migration in the Jetpack plugin seeds this option from the site's existing module
99 * state and keeps it in sync while the legacy module still exists. See
100 * `Jetpack::migrate_canonical_urls_module_to_seo_option()`.
101 *
102 * @var string
103 */
104 const CANONICAL_ENABLED_OPTION = 'jetpack_seo_canonical_urls_enabled';
105
106 /**
107 * Option recording whether the Jetpack SEO surface is discoverable on this site.
108 *
109 * Gates whether the SEO admin menu registers on self-hosted sites. Seeded once by the
110 * Jetpack plugin on install/upgrade: fresh installs default to visible, existing
111 * installs default to hidden and opt in via the legacy Traffic page or My Jetpack.
112 * WordPress.com (Simple + Atomic) bypasses this option entirely and is always visible.
113 * Absent until seeded, in which case self-hosted defaults to hidden (the non-disruptive
114 * default). See {@see Surface_Visibility::is_visible()}.
115 *
116 * @var string
117 */
118 const VISIBILITY_OPTION = 'jetpack_seo_surface_visible';
119
120 /**
121 * Whether the package has been initialized.
122 *
123 * @var bool
124 */
125 private static $initialized = false;
126
127 /**
128 * Initialize the package.
129 *
130 * Called from the Jetpack plugin's `late_initialization()` hook.
131 *
132 * @return void
133 */
134 public static function init() {
135 if ( self::$initialized ) {
136 return;
137 }
138 self::$initialized = true;
139
140 // Gate the entire SEO surface behind its legacy filter or per-site feature.
141 if ( ! self::is_available() ) {
142 return;
143 }
144
145 // The opt-in endpoint must be reachable even before the surface is visible, so
146 // existing self-hosted installs can switch to the new experience from the legacy
147 // Traffic page or My Jetpack (JETPACK-1700). Registered ahead of the cohort gate.
148 add_action( 'rest_api_init', array( Surface_Visibility::class, 'register_optin_route' ) );
149
150 // Expose opt-in availability to other admin surfaces (the legacy Traffic-page
151 // banner reads it via `@automattic/jetpack-script-data`). Hooked here — after the
152 // feature flag, before the cohort gate — so a still-hidden install gets the signal.
153 add_filter( 'jetpack_admin_js_script_data', array( Surface_Visibility::class, 'inject_optin_availability' ) );
154
155 // Sitemap output is a front-end concern tied to the SEO feature itself, not to
156 // whether the admin dashboard is visible — so register it here, ahead of the
157 // cohort gate. This keeps the deliberate-off behavior consistent in the two
158 // edges the surface gate would otherwise break: a site that turns the sitemap
159 // off while the dashboard is still hidden (an existing self-hosted install that
160 // hasn't opted in), and a flag set while the dashboard was visible that must
161 // stay honored if the dashboard is later hidden.
162 //
163 // Maintain the deliberate-off flag as the sitemap is toggled: these fire only on
164 // a genuine module toggle (not wpcomsh's private-site suppression, which is a
165 // filter, not a deactivation), and are registered before the toggle's REST write.
166 add_action( 'jetpack_deactivate_module_sitemaps', array( __CLASS__, 'flag_sitemap_user_disabled' ) );
167 add_action( 'jetpack_activate_module_sitemaps', array( __CLASS__, 'clear_sitemap_user_disabled' ) );
168
169 // When the user has deliberately turned the sitemap off, suppress WordPress
170 // core's own sitemap too — otherwise "off" silently falls back to core's
171 // `/wp-sitemap.xml` (and its `/sitemap.xml` → `/wp-sitemap.xml` redirect). Keyed
172 // on the deliberate-off flag, NOT the ambient off-state, so a site that never
173 // enabled the sitemap keeps whatever sitemap it already had. Runs on
174 // `plugins_loaded`, before core registers its sitemap server on `init`, so the
175 // filter is in place; with core sitemaps disabled, `/sitemap.xml` and
176 // `/wp-sitemap.xml` both return a proper 404. (When the sitemap is ON, the
177 // Jetpack sitemaps module already disables core's duplicate.)
178 if ( get_option( self::SUPPRESS_WP_SITEMAP_OPTION, false ) ) {
179 add_filter( 'wp_sitemaps_enabled', '__return_false' );
180 }
181
182 // Discoverability cohort gate: the SEO surface is auto-discoverable for fresh
183 // installs and all WordPress.com sites; existing self-hosted installs opt in via
184 // the legacy Traffic page or My Jetpack (JETPACK-1700). Until it's visible we
185 // register nothing else here and let those opt-in surfaces drive discovery.
186 if ( ! self::is_seo_surface_visible() ) {
187 return;
188 }
189
190 // The admin menu and app shell register whenever the surface is visible, even
191 // when the `seo-tools` module is inactive, so SEO stays discoverable and can be
192 // turned on from within the page itself (JETPACK-1700). When the module is off,
193 // the Overview renders only its "enable SEO tools" affordance.
194 //
195 // Priority 1: load the wp-build bundle (and define its render function)
196 // before `add_menu_item()` runs at the default priority and needs it.
197 add_action( 'admin_menu', array( Admin_Page::class, 'maybe_load_wp_build' ), 1 );
198 add_action( 'admin_menu', array( Admin_Page::class, 'add_menu_item' ), 10 );
199
200 // Read-only REST routes the dashboard hydrates its initial state from. Preloaded
201 // into the page (see Admin_Page::inject_script_data) so a normal load resolves
202 // them with no request, and fetched by the app when that preload is missing or
203 // stale — so the dashboard recovers its data instead of dead-ending. Registered
204 // whenever the surface is visible (independent of the seo-tools module, like the
205 // Overview).
206 add_action( 'rest_api_init', array( Dashboard_Data::class, 'register_rest_reads' ) );
207
208 // Keep the Overview's cached content-coverage counts honest. Hooked here rather than
209 // alongside the admin surface above because posts are written from everywhere — the
210 // block editor (REST), the classic editor, wp-cli, cron, other plugins — and the
211 // cache has to be dropped wherever that happens, not just where it's read.
212 Content_Coverage::register_invalidation();
213
214 // The settings surface only comes online once SEO tools are active — there's
215 // nothing to configure while the module is off, so we don't register its REST
216 // endpoints until then. Expose the core `blog_public` option to the REST settings
217 // endpoint so the Settings tab can save search-engine visibility via
218 // `/wp/v2/settings` (the Jetpack settings endpoint only accepts Jetpack options).
219 // Writes are still capability-gated by the core settings controller.
220 if ( self::is_seo_tools_module_active() ) {
221 // Front-end JSON-LD schema output and author profile schema fields.
222 // Intentionally NOT gated: every site keeps emitting its structured data —
223 // a plan-gated site loses the schema *settings* card (a paid control), but
224 // stripping the schema its pages already carry would hurt SEO it has today.
225 // (Finer per-type gating — e.g. sitewide LocalBusiness to paid plans on
226 // self-hosted — is a separate follow-up, tracked in the schema project.)
227 Schema_Builder::init();
228 Author_Schema_Node::init();
229
230 // GEO-tab front-end services. These are paid surfaces on WordPress.com: a
231 // plan-gated site has the GEO tab hidden from its dashboard, so it must not
232 // keep emitting their front-end output either — otherwise it would still
233 // serve /llms.txt and AI-crawler robots.txt directives it doesn't qualify
234 // for. Self-hosted is never gated, so it always registers both.
235 if ( ! self::is_gated() ) {
236 // The /llms.txt handler. Self-hooks a front-end action, so it no-ops off
237 // the front end and stays behind the same gates as the schema above.
238 Llms_Txt::init();
239 // robots.txt directives for blocked AI crawlers. Self-hooks the
240 // `robots_txt` filter, so it stays inert off the front end.
241 Ai_Crawlers::init();
242 }
243
244 add_action( 'rest_api_init', array( Dashboard_Data::class, 'register_rest_settings' ) );
245 // Package-owned route for the site-level Schema settings (see the controller).
246 add_action( 'rest_api_init', array( Schema_Settings_Controller::class, 'register_routes' ) );
247 }
248
249 /**
250 * Fires after the Jetpack SEO package is initialized.
251 *
252 * @since 0.1.0
253 */
254 do_action( 'jetpack_seo_init' );
255 }
256
257 /**
258 * Whether the Jetpack SEO product is available on this site.
259 *
260 * Keep the existing filter as an override while allowing WordPress.com to
261 * enable the product for individual sites through its feature registry.
262 *
263 * @return bool
264 */
265 public static function is_available() {
266 if ( (bool) apply_filters( self::FEATURE_FILTER, false ) ) {
267 return true;
268 }
269
270 $features = ( new Host() )->is_wpcom_simple()
271 ? Current_Plan::get_simple_site_specific_features()
272 : Current_Plan::get()['features'];
273
274 return in_array( self::FEATURE_SLUG, $features['active'] ?? array(), true );
275 }
276
277 /**
278 * Whether the Jetpack SEO surface should be discoverable (admin menu registered).
279 *
280 * @return bool
281 */
282 public static function is_seo_surface_visible() {
283 return Surface_Visibility::is_visible();
284 }
285
286 /**
287 * Whether to offer an existing install the chance to opt into the new SEO experience.
288 *
289 * @return bool
290 */
291 public static function is_optin_available() {
292 return Surface_Visibility::is_optin_available();
293 }
294
295 /**
296 * Whether the SEO dashboard is plan-gated for this site.
297 *
298 * Gating applies only on WordPress.com (Simple + Atomic): `advanced-seo` is in the
299 * FREE plan's supports list, so `Current_Plan::supports( 'advanced-seo' )` returns
300 * true on self-hosted (never gated) and hijacks to `wpcom_site_has_feature()` on
301 * WordPress.com, where it's false below the Premium plan. Mirrors the AI SEO
302 * Enhancer's plan check in {@see Dashboard_Data::get_ai_data()}.
303 *
304 * Public because {@see Admin_Page::inject_script_data()} reads it to build the
305 * dashboard's gating payload, and {@see self::init()} uses it to decide whether the
306 * GEO-tab front-end services register at all.
307 *
308 * @return bool
309 */
310 public static function is_gated() {
311 return ( new Host() )->is_wpcom_platform()
312 && ! Current_Plan::supports( 'advanced-seo' );
313 }
314
315 /**
316 * The WordPress.com Premium checkout URL for this site, used by the upsell banner
317 * shown to gated sites.
318 *
319 * Built server-side because the client doesn't have the site slug. `value_bundle`
320 * is the wpcom Premium plan slug (see the `premium` entry in
321 * `Automattic\Jetpack\Current_Plan`), and `Status::get_site_suffix()` resolves the
322 * Calypso site slug (via `WPCOM_Masterbar::get_calypso_site_slug()` on wpcom).
323 *
324 * @return string
325 */
326 public static function get_upsell_url() {
327 $site_slug = ( new Status() )->get_site_suffix();
328
329 return sprintf( 'https://wordpress.com/checkout/%s/value_bundle', $site_slug );
330 }
331
332 /**
333 * Whether the `seo-tools` Jetpack module is currently active.
334 *
335 * @return bool
336 */
337 private static function is_seo_tools_module_active() {
338 if ( ! class_exists( 'Automattic\\Jetpack\\Modules' ) ) {
339 return false;
340 }
341 return ( new Modules() )->is_active( 'seo-tools' );
342 }
343
344 /**
345 * Record that the user has turned the sitemap off, so WordPress core's own sitemap
346 * is suppressed too. Hooked to the sitemaps module's deactivation, which fires only
347 * on a real toggle from a surface (the SEO Settings tab, the legacy Traffic page, or
348 * WP-CLI) — not wpcomsh's private-site suppression, which is a filter on the
349 * active-modules read rather than a deactivation.
350 *
351 * @return void
352 */
353 public static function flag_sitemap_user_disabled() {
354 update_option( self::SUPPRESS_WP_SITEMAP_OPTION, true );
355 }
356
357 /**
358 * Clear the deliberate-off flag when the sitemap is turned back on — the Jetpack
359 * sitemaps module then serves `/sitemap.xml` and suppresses core's duplicate itself.
360 *
361 * @return void
362 */
363 public static function clear_sitemap_user_disabled() {
364 delete_option( self::SUPPRESS_WP_SITEMAP_OPTION );
365 }
366 }
367