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