PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-seo / src / class-dashboard-data.php
jetpack / jetpack_vendor / automattic / jetpack-seo / src Last commit date
class-admin-page.php 3 weeks ago class-ai-crawlers.php 3 weeks ago class-ai-seo-enhancer.php 3 days ago class-author-schema-node.php 1 month ago class-breadcrumb-schema-node.php 1 month ago class-content-coverage.php 3 weeks ago class-dashboard-data.php 2 weeks ago class-initializer.php 3 days ago class-llms-txt.php 3 weeks ago class-local-business-schema-node.php 1 month ago class-organization-schema-node.php 1 month ago class-post-schema-node.php 1 month ago class-post-types.php 3 weeks ago class-schema-builder.php 1 month ago class-schema-graph.php 1 month ago class-schema-node-ids.php 1 month ago class-schema-settings-controller.php 1 month ago class-schema-settings.php 1 month ago class-surface-visibility.php 2 weeks ago class-website-schema-node.php 1 month ago
class-dashboard-data.php
464 lines
1 <?php
2 /**
3 * The SEO dashboard's read-only REST routes and the payload builders behind
4 * them — one route per data-backed tab, preloaded onto the page so a normal
5 * load resolves them with no request.
6 *
7 * @package automattic/jetpack-seo-package
8 */
9
10 namespace Automattic\Jetpack\SEO;
11
12 use Automattic\Jetpack\Modules;
13 use Jetpack_SEO_Utils;
14
15 /**
16 * Registers the dashboard's read routes and builds their payloads.
17 */
18 class Dashboard_Data {
19
20 /**
21 * Map of read-only dashboard routes: tab slug => data-builder callable. The
22 * single source of truth for both the registered routes and the paths
23 * preloaded onto the page, so the two can't drift.
24 *
25 * @return array<string, callable>
26 */
27 private static function rest_reads() {
28 return array(
29 'overview' => array( __CLASS__, 'get_overview_data' ),
30 'settings' => array( __CLASS__, 'get_settings_data' ),
31 'ai' => array( __CLASS__, 'get_ai_data' ),
32 'content' => array( __CLASS__, 'get_content_data' ),
33 );
34 }
35
36 /**
37 * REST paths the dashboard reads its initial state from, preloaded into the
38 * page (see {@see Admin_Page::inject_script_data()}) and fetched by the app.
39 *
40 * @return string[]
41 */
42 public static function rest_read_paths() {
43 return array_map(
44 static function ( $slug ) {
45 return '/jetpack/v4/seo/' . $slug;
46 },
47 array_keys( self::rest_reads() )
48 );
49 }
50
51 /**
52 * Register the read-only REST routes the dashboard hydrates from — one per
53 * data-backed tab, each returning the same builder payload previously injected
54 * synchronously onto the page. Read-only and gated to the page's own
55 * `manage_options`; writes still go through their existing endpoints.
56 *
57 * @return void
58 */
59 public static function register_rest_reads() {
60 foreach ( self::rest_reads() as $slug => $builder ) {
61 register_rest_route(
62 'jetpack/v4',
63 '/seo/' . $slug,
64 array(
65 'methods' => \WP_REST_Server::READABLE,
66 'callback' => static function () use ( $builder ) {
67 return rest_ensure_response( call_user_func( $builder ) );
68 },
69 'permission_callback' => array( __CLASS__, 'reads_permission_check' ),
70 )
71 );
72 }
73 }
74
75 /**
76 * Capability gate for the dashboard's read routes — the same `manage_options`
77 * the SEO admin page itself requires.
78 *
79 * @return bool
80 */
81 public static function reads_permission_check() {
82 return current_user_can( 'manage_options' );
83 }
84
85 /**
86 * Expose the core `blog_public` option to the REST settings endpoint.
87 *
88 * Search-engine visibility is a WordPress core option, not a Jetpack one,
89 * so the Settings tab saves it through `/wp/v2/settings` — which only
90 * round-trips settings registered with `show_in_rest`. The core settings
91 * controller enforces the `manage_options` capability on writes.
92 *
93 * @return void
94 */
95 public static function register_rest_settings() {
96 register_setting(
97 'reading',
98 'blog_public',
99 array(
100 'show_in_rest' => true,
101 'type' => 'integer',
102 'default' => 1,
103 )
104 );
105 }
106
107 /**
108 * Build the aggregated Overview state the dashboard renders.
109 *
110 * @return array
111 */
112 public static function get_overview_data() {
113 $modules = new Modules();
114 // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists.
115 $seo_enabled = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo();
116
117 $codes = get_option( 'verification_services_codes', array() );
118 if ( ! is_array( $codes ) ) {
119 $codes = array();
120 }
121
122 return array(
123 'site_visibility' => array(
124 'search_engines_visible' => (int) get_option( 'blog_public', 1 ) === 1,
125 // Read the durable SEO option (seeded/synced from the `sitemaps` module
126 // by the Jetpack plugin) so the state survives the module's removal. The
127 // reachable sitemap URL + "View" link live on the Settings tab.
128 'sitemap_active' => self::is_sitemap_enabled( $modules ),
129 'seo_tools_active' => $modules->is_active( 'seo-tools' ),
130 ),
131 // Per-service booleans (a code is set or not) for the Overview's
132 // Site verification card.
133 'site_verification' => array(
134 'google' => ! empty( $codes['google'] ),
135 'bing' => ! empty( $codes['bing'] ),
136 'pinterest' => ! empty( $codes['pinterest'] ),
137 'yandex' => ! empty( $codes['yandex'] ),
138 'facebook' => ! empty( $codes['facebook'] ),
139 ),
140 'content_coverage' => Content_Coverage::get(),
141 'plan' => array(
142 'seo_enabled_for_site' => $seo_enabled,
143 ),
144 );
145 }
146
147 /**
148 * Build the editable Settings state the Settings tab hydrates from.
149 *
150 * Read-only bootstrap only. Most writes go through the existing
151 * `/jetpack/v4/settings` REST endpoint, which already validates and
152 * sanitizes those flat fields. Nested Schema writes use the package's
153 * schema-settings route; bootstrapping them here keeps the Settings UI
154 * hydrated without a second request.
155 *
156 * @return array
157 */
158 public static function get_settings_data() {
159 $modules = new Modules();
160
161 // Read the stored values directly: Jetpack_SEO_Titles::get_custom_title_formats()
162 // intentionally hides them while another SEO plugin controls output, but the
163 // dashboard must still show the saved values without allowing edits.
164 $title_formats = get_option( 'advanced_seo_title_formats', array() );
165 if ( ! is_array( $title_formats ) ) {
166 $title_formats = array();
167 }
168 // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists.
169 $title_formats_editable = class_exists( 'Jetpack_SEO_Utils' ) && Jetpack_SEO_Utils::is_enabled_jetpack_seo();
170 // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists.
171 $front_page_desc = class_exists( 'Jetpack_SEO_Utils' ) ? Jetpack_SEO_Utils::get_front_page_meta_description() : '';
172
173 // A site that set a front-page description back when it was free for all
174 // WordPress.com Simple sites keeps editing it, even when otherwise plan-gated:
175 // the value stays live and `Jetpack_SEO_Utils` still reads/writes it via the
176 // legacy option. The gated Settings uses this to keep that one field editable.
177 // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Utils lives in plugins/jetpack and is guarded by class_exists.
178 $has_legacy_front_page_meta = class_exists( 'Jetpack_SEO_Utils' ) && (bool) Jetpack_SEO_Utils::has_legacy_front_page_meta();
179
180 $codes = get_option( 'verification_services_codes', array() );
181 if ( ! is_array( $codes ) ) {
182 $codes = array();
183 }
184
185 $sitemap_active = self::is_sitemap_enabled( $modules );
186
187 return array(
188 'search_engines_visible' => (int) get_option( 'blog_public', 1 ) === 1,
189 // Read the durable SEO option (seeded/synced from the `sitemaps` module
190 // by the Jetpack plugin) so the state survives the module's removal.
191 'sitemap_active' => $sitemap_active,
192 // The reachable sitemap URL (Jetpack serves a valid sitemap here as soon as
193 // it's on + the site is public), or '' when sitemaps are off, so the Settings
194 // tab shows the "View sitemap" link exactly when there's a sitemap to view.
195 'sitemap_url' => self::get_reachable_sitemap_url( $sitemap_active ),
196 // Read the durable SEO option (seeded/synced from the `canonical-urls` module
197 // by the Jetpack plugin) so the state survives the module's removal.
198 'canonical_active' => self::is_canonical_enabled( $modules ),
199 // Cast to object so an empty format set serializes as `{}`, not `[]`.
200 'title_formats' => (object) $title_formats,
201 // Separator WordPress joins default document-title parts with. A page type
202 // with no stored format keeps the default title: `get_custom_title()` returns
203 // the incoming value untouched, so core composes the title itself and the
204 // Settings tab replays that composition to preview it.
205 'title_separator' => self::get_default_title_separator(),
206 'title_formats_editable' => $title_formats_editable,
207 'front_page_description' => (string) $front_page_desc,
208 'has_legacy_front_page_meta' => $has_legacy_front_page_meta,
209 'verification_tools_active' => $modules->is_active( 'verification-tools' ),
210 'verification' => array(
211 'google' => isset( $codes['google'] ) ? (string) $codes['google'] : '',
212 'bing' => isset( $codes['bing'] ) ? (string) $codes['bing'] : '',
213 'pinterest' => isset( $codes['pinterest'] ) ? (string) $codes['pinterest'] : '',
214 'yandex' => isset( $codes['yandex'] ) ? (string) $codes['yandex'] : '',
215 'facebook' => isset( $codes['facebook'] ) ? (string) $codes['facebook'] : '',
216 ),
217 'schema' => Schema_Settings::get_editable(),
218 );
219 }
220
221 /**
222 * Build the Google site-verification state for the Settings tab.
223 *
224 * The Settings verification card lets a connected user verify with Google via a
225 * WordPress.com keyring OAuth popup (in addition to pasting a meta-tag code). This
226 * bootstraps the keyring connect URL and whether the current user is connected —
227 * the live verified status is fetched client-side from `/jetpack/v4/verify-site/google`
228 * (a wpcom round-trip we don't want to make on every page load).
229 *
230 * Both `Keyring_Helper` (Publicize package) and the connection `Manager` are provided
231 * by the host Jetpack plugin, so they're guarded with `class_exists` like the
232 * `Jetpack_SEO_*` helpers. On a disconnected self-hosted site `is_connected` is false
233 * and the UI falls back to manual code entry only.
234 *
235 * @return array
236 */
237 public static function get_google_verify_data() {
238 $connect_url = '';
239 if ( class_exists( 'Automattic\\Jetpack\\Publicize\\Keyring_Helper' ) ) {
240 // @phan-suppress-next-line PhanUndeclaredClassMethod -- guarded; Publicize package is provided by the host plugin.
241 $connect_url = (string) \Automattic\Jetpack\Publicize\Keyring_Helper::connect_url( 'google_site_verification', 'other' );
242 }
243
244 $is_connected = false;
245 if ( class_exists( 'Automattic\\Jetpack\\Connection\\Manager' ) ) {
246 $is_connected = ( new \Automattic\Jetpack\Connection\Manager() )->is_user_connected();
247 }
248
249 return array(
250 'connect_url' => $connect_url,
251 'is_connected' => (bool) $is_connected,
252 );
253 }
254
255 /**
256 * Build the AI tab's initial state.
257 *
258 * The AI SEO Enhancer auto-generates SEO titles/descriptions/alt-text in the
259 * editor (the generation itself is wpcom/AI-Assistant side); this exposes only
260 * its persisted on/off toggle and whether it's available. Availability mirrors
261 * the legacy Traffic page: the `ai_seo_enhancer_enabled` feature filter must be
262 * on (it still depends on AI being available) AND the site's plan must support
263 * the `ai-seo-enhancer` feature. The toggle writes through the existing
264 * `/jetpack/v4/settings` endpoint (`ai_seo_enhancer_enabled`).
265 *
266 * @return array
267 */
268 public static function get_ai_data() {
269 $filter_on = (bool) apply_filters( 'ai_seo_enhancer_enabled', true );
270
271 // Current_Plan comes from the jetpack-plans package (a dependency of this
272 // package since the plan-gating work), so it's always available here; the
273 // class_exists guard is kept as belt-and-suspenders for older bundled snapshots.
274 $plan_supports = class_exists( 'Automattic\\Jetpack\\Current_Plan' )
275 && \Automattic\Jetpack\Current_Plan::supports( 'ai-seo-enhancer' );
276
277 return array(
278 'enhancer' => array(
279 'available' => $filter_on && $plan_supports,
280 'enabled' => (bool) get_option( 'ai_seo_enhancer_enabled', false ),
281 ),
282 'llmsTxt' => array(
283 'enabled' => Llms_Txt::is_enabled(),
284 'url' => home_url( '/llms.txt' ),
285 'canServe' => Llms_Txt::can_serve(),
286 ),
287 'crawlers' => Ai_Crawlers::get_bootstrap_data(),
288 );
289 }
290
291 /**
292 * Build the supported post type options for the Content tab.
293 *
294 * @return array{post_types:array<int,array{slug:string,label:string}>}
295 */
296 public static function get_content_data() {
297 return array(
298 'post_types' => Post_Types::get_supported_content_type_options(),
299 );
300 }
301
302 /**
303 * Site identity used to render the homepage search/social previews on the
304 * Settings tab: title, tagline, URL, and representative images. The front-page
305 * description that completes the preview is read from the Settings form
306 * (it's editable there), not bootstrapped here.
307 *
308 * @return array
309 */
310 public static function get_site_data() {
311 $icon_url = (string) get_site_icon_url();
312
313 $logo_id = (int) get_theme_mod( 'custom_logo' );
314 $logo_url = $logo_id ? (string) wp_get_attachment_image_url( $logo_id, 'full' ) : '';
315 if ( class_exists( 'Jetpack_Redux_State_Helper' ) ) {
316 // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_Redux_State_Helper lives in plugins/jetpack and is guarded by class_exists.
317 $image_url = (string) \Jetpack_Redux_State_Helper::get_site_image();
318 } else {
319 $image_url = $logo_url ? $logo_url : $icon_url;
320 }
321
322 return array(
323 'title' => (string) get_bloginfo( 'name' ),
324 'tagline' => (string) get_bloginfo( 'description' ),
325 'url' => (string) home_url(),
326 'icon' => $icon_url,
327 'image' => $image_url,
328 );
329 }
330
331 /**
332 * Whether sitemap generation is enabled.
333 *
334 * Reads the durable {@see Initializer::SITEMAP_ENABLED_OPTION} flag. The default is only
335 * used when the option is absent (for example before the Jetpack plugin's migration
336 * has run on a freshly upgraded site), in which case it falls back to the live
337 * `sitemaps` module state so behavior is unchanged in that gap.
338 *
339 * @param Modules $modules Modules instance to read live module state from.
340 * @return bool
341 */
342 private static function is_sitemap_enabled( Modules $modules ) {
343 $enabled = get_option( Initializer::SITEMAP_ENABLED_OPTION, null );
344
345 // Only fall back to the live module state when the durable option is absent.
346 // Passing it as get_option()'s default would evaluate it on every call, since
347 // PHP resolves function arguments eagerly even when the option exists.
348 if ( null === $enabled ) {
349 $enabled = $modules->is_active( 'sitemaps' );
350 }
351
352 return (bool) $enabled;
353 }
354
355 /**
356 * Whether canonical URLs are enabled.
357 *
358 * Reads the durable {@see Initializer::CANONICAL_ENABLED_OPTION} flag. The default is only
359 * used when the option is absent (for example before the Jetpack plugin's migration
360 * has run on a freshly upgraded site), in which case it falls back to the live
361 * `canonical-urls` module state so behavior is unchanged in that gap.
362 *
363 * @param Modules $modules Modules instance to read live module state from.
364 * @return bool
365 */
366 private static function is_canonical_enabled( Modules $modules ) {
367 $enabled = get_option( Initializer::CANONICAL_ENABLED_OPTION, null );
368
369 // Only fall back to the live module state when the durable option is absent.
370 // Passing it as get_option()'s default would evaluate it on every call, since
371 // PHP resolves function arguments eagerly even when the option exists.
372 if ( null === $enabled ) {
373 $enabled = $modules->is_active( 'canonical-urls' );
374 }
375
376 return (bool) $enabled;
377 }
378
379 /**
380 * The separator, as rendered, that WordPress joins default document-title parts
381 * with — for previewing the title a page type with no stored format produces.
382 *
383 * `document_title_separator` alone is not what a visitor sees. `wp_get_document_title()`
384 * composes the parts, then passes the whole title through the `document_title`
385 * filter, which WordPress texturizes by default — turning the default spaced
386 * hyphen into an en dash. Previewing the raw filter value would show `-` on a site
387 * that renders `–`, which is every site running core's defaults.
388 *
389 * This applies only to the default title. A stored format short-circuits
390 * `pre_get_document_title`, which returns before the `document_title` filter, so a
391 * custom format keeps the separator the user typed verbatim — the front end renders
392 * `Site - MARKER - Page` for a custom format while producing `Page – Site` for the
393 * default one.
394 *
395 * @return string The rendered separator.
396 */
397 private static function get_default_title_separator() {
398 $separator = (string) apply_filters( 'document_title_separator', '-' );
399
400 // Only texturize when the title itself would be: a site that unhooks
401 // `wptexturize` renders the raw separator, and the preview should match.
402 if ( has_filter( 'document_title', 'wptexturize' ) ) {
403 $separator = trim(
404 html_entity_decode( wptexturize( ' ' . $separator . ' ' ), ENT_QUOTES, 'UTF-8' )
405 );
406 }
407
408 return $separator;
409 }
410
411 /**
412 * The public URL of the XML sitemap, or an empty string when none is reachable.
413 *
414 * A sitemap is reachable as soon as generation is enabled and the site is public:
415 * Jetpack serves a valid (empty-until-built) sitemap at a stable URL — never a 404 —
416 * so the link is safe to surface immediately, without waiting on (or gating against)
417 * the cron build. A prior gate looked the master sitemap up by a mis-built filename
418 * and so never matched, which is what left the Settings tab stuck on "Generating…".
419 *
420 * `jetpack_sitemap_uri()` / `jp_sitemap_filename()` and the JP_MASTER_SITEMAP_TYPE
421 * constant live in the Jetpack plugin's Sitemaps module (loaded only for an active
422 * module on a public site), so they are guarded; in the package-only context they
423 * are absent and the sitemap is reported as not reachable.
424 *
425 * @param bool $sitemap_active Whether sitemap generation is enabled.
426 * @return string The sitemap URL, or '' when not reachable.
427 */
428 private static function get_reachable_sitemap_url( $sitemap_active ) {
429 // Jetpack only serves sitemaps when generation is on and the site is public.
430 if ( ! $sitemap_active || (int) get_option( 'blog_public', 1 ) !== 1 ) {
431 return '';
432 }
433
434 // The `JP_MASTER_SITEMAP_TYPE` constant and the `jp_sitemap_filename()` /
435 // `jetpack_sitemap_uri()` helpers all live together in plugins/jetpack and load
436 // as a unit, so this single guard covers every symbol used below.
437 if (
438 ! defined( 'JP_MASTER_SITEMAP_TYPE' )
439 || ! function_exists( 'jp_sitemap_filename' )
440 || ! function_exists( 'jetpack_sitemap_uri' )
441 ) {
442 return '';
443 }
444
445 // `jp_sitemap_filename()` returns an error string ("error-not-int-…") unless a
446 // non-null number is passed; the master ignores the number, so pass 0 (matching
447 // Jetpack's own call sites). Fail safe: the master file is always 'sitemap.xml',
448 // so if a bundled Jetpack ever returns something else, report not-reachable
449 // rather than surface a broken URL — the exact failure this method shipped with
450 // before (the missing number silently produced an "error-not-int-…" link).
451 // @phan-suppress-next-line PhanUndeclaredFunction -- guarded above; symbols live in plugins/jetpack.
452 $filename = (string) jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, 0 );
453 if ( 'sitemap.xml' !== $filename ) {
454 return '';
455 }
456
457 // esc_url_raw (not esc_url): transported via script data and rendered by React,
458 // so it must not be HTML-entity-encoded (e.g. the plain-permalink
459 // `?jetpack-sitemap=` form keeps its raw `&`).
460 // @phan-suppress-next-line PhanUndeclaredFunction -- guarded above; symbols live in plugins/jetpack.
461 return esc_url_raw( (string) jetpack_sitemap_uri( $filename ) );
462 }
463 }
464